Skip to content

fix(install): add PostToolUse/Stop/PreCompact/UserPromptSubmit hooks to Claude Code adapter#243

Open
Gradata wants to merge 1 commit into
mainfrom
fix/gra-1233-posttooluse-capture-hook
Open

fix(install): add PostToolUse/Stop/PreCompact/UserPromptSubmit hooks to Claude Code adapter#243
Gradata wants to merge 1 commit into
mainfrom
fix/gra-1233-posttooluse-capture-hook

Conversation

@Gradata
Copy link
Copy Markdown
Owner

@Gradata Gradata commented Jun 2, 2026

Closes GRA-1233

Problem

gradata install --agent claude-code only installed PreToolUse hooks. PostToolUse auto_correct was missing — no correction capture on install.

Fix

  • Added auto_correct_command, session_close_command, pre_compact_command, context_inject_command to _base.py
  • Updated claude_code.py install() to write all 5 lifecycle hooks with per-lifecycle idempotency
  • Updated uninstall() to clean all 5 instead of just PreToolUse

Test

test_hook_adapters.py — 10/10 pass, including test_claude_code_install_writes_pre_compact_entry

…to Claude Code adapter

GRA-1233 — 'gradata install --agent claude-code' was installing ONLY PreToolUse,
missing the PostToolUse auto_correct, Stop session_close, PreCompact snapshot,
and UserPromptSubmit context_inject hooks. Core value proposition broken at install.

Added to _base.py: auto_correct_command, session_close_command, pre_compact_command,
context_inject_command helpers. Updated claude_code.py install() to write all 5
lifecycle hooks (with idempotency checks per-lifecycle). Updated uninstall() to
clean all 5 instead of just PreToolUse.

Test: test_hook_adapters.py test_claude_code_install_writes_pre_compact_entry
validates PreCompact entry. Full adapter contract test covers all agents.
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

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
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR extends the hook adapter system to install Claude Code hooks across five lifecycle phases—PreToolUse, PostToolUse, Stop, PreCompact, and UserPromptSubmit—instead of only PreToolUse. It adds shared command-building helpers and refactors install/uninstall logic to wire and remove hooks from all phases, with test coverage validating the new behavior.

Changes

Multi-phase hook wiring foundation and implementation

Layer / File(s) Summary
Command helper functions for hook phase entry points
Gradata/src/gradata/hooks/adapters/_base.py
Four new module-level helpers (auto_correct_command, session_close_command, pre_compact_command, context_inject_command) generate shell command strings that invoke python -m gradata.hooks.* entrypoints with BRAIN_DIR set and proper quoting.
Claude Code multi-phase hook installation and removal
Gradata/src/gradata/hooks/adapters/claude_code.py
Imports new Claude Code command identifiers; refactors install() to create and wire hooks across five lifecycle keys with signature-presence checks and conditional append logic; refactors uninstall() to remove signature-matching entries from all five phases in a single loop and prune empty lifecycle lists.
Hook adapter test coverage
Gradata/tests/test_hook_adapters.py
Adds json import and extends Codex adapter test with hook structure assertions; introduces tests for Opencode adapter idempotency and pre/post/session hook wiring, and for Claude Code idempotent installation with a single PreCompact hook containing brain directory injection and the pre_compact command.

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested labels

bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% 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 directly matches the main change: adding multiple lifecycle hooks (PostToolUse/Stop/PreCompact/UserPromptSubmit) to the Claude Code adapter installation.
Description check ✅ Passed The description is directly related to the changeset, explaining the problem, fix, and test coverage for adding missing lifecycle hooks to the Claude Code adapter.
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 fix/gra-1233-posttooluse-capture-hook

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.17][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 bug Something isn't working label Jun 2, 2026
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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Gradata/src/gradata/hooks/adapters/_base.py (1)

132-164: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consolidate the five near-identical command builders.

hook_command, auto_correct_command, session_close_command, pre_compact_command, and context_inject_command differ only in the trailing module name. A single parametrized helper removes the copy-paste and keeps quoting/BRAIN_DIR logic in one place as more lifecycle phases are added.

♻️ Proposed consolidation
+def _module_command(brain_dir: Path, module: str) -> str:
+    return (
+        f"BRAIN_DIR={shlex.quote(str(brain_dir))} "
+        f"{shlex.quote(sys.executable)} -m gradata.hooks.{module}"
+    )
+
+
+def hook_command(brain_dir: Path) -> str:
+    return _module_command(brain_dir, "inject_brain_rules")
+
+
+def auto_correct_command(brain_dir: Path) -> str:
+    return _module_command(brain_dir, "auto_correct")
+
+
+def session_close_command(brain_dir: Path) -> str:
+    return _module_command(brain_dir, "session_close")
+
+
+def pre_compact_command(brain_dir: Path) -> str:
+    return _module_command(brain_dir, "pre_compact")
+
+
+def context_inject_command(brain_dir: Path) -> str:
+    return _module_command(brain_dir, "context_inject")
🤖 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 `@Gradata/src/gradata/hooks/adapters/_base.py` around lines 132 - 164,
Consolidate the five near-identical builders by adding a single helper (e.g.,
build_hook_command or command_for_module) that accepts the module name and
brain_dir and returns the quoted command using shlex.quote(str(brain_dir)),
shlex.quote(sys.executable) and the "-m gradata.hooks.<module>" suffix; then
refactor hook_command, auto_correct_command, session_close_command,
pre_compact_command, and context_inject_command to each call that helper with
their respective module string (e.g., "inject_brain_rules", "auto_correct",
"session_close", "pre_compact", "context_inject") so all BRAIN_DIR and quoting
logic is centralized.
🤖 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`:
- Around line 91-103: The PostToolUse hook's matcher only matches "Edit" or
"Write" so MultiEdit never triggers; update the matcher used when building
post_tool (variable post_tool in claude_code.py) to include "MultiEdit" (e.g.,
change "matcher": "Edit|Write" to include MultiEdit) so the hook fires for
EDIT_TOOLS entries like "MultiEdit" handled by extract_correction(); ensure the
auto_correct_command(brain_dir) hook entry (id: sig) remains unchanged.

In `@Gradata/tests/test_hook_adapters.py`:
- Around line 92-112: The test test_claude_code_install_writes_pre_compact_entry
currently only asserts the PreCompact hook; extend it to also assert that a
PostToolUse entry is written and contains the expected matcher/command to cover
the auto_correct wiring: after loading settings =
json.loads(config_path.read_text(...)) check settings["hooks"]["PostToolUse"]
exists, inspect its entries for the expected matcher (e.g., "MultiEdit" / the
matcher name used in claude_code.py) and that one of its commands contains the
PostToolUse-related module path (e.g., "gradata.hooks.post_tool_use" or the
exact command string produced by get_adapter("claude-code").install), mirroring
the existing pattern used for PreCompact to lock in the multi-phase contract.

---

Outside diff comments:
In `@Gradata/src/gradata/hooks/adapters/_base.py`:
- Around line 132-164: Consolidate the five near-identical builders by adding a
single helper (e.g., build_hook_command or command_for_module) that accepts the
module name and brain_dir and returns the quoted command using
shlex.quote(str(brain_dir)), shlex.quote(sys.executable) and the "-m
gradata.hooks.<module>" suffix; then refactor hook_command,
auto_correct_command, session_close_command, pre_compact_command, and
context_inject_command to each call that helper with their respective module
string (e.g., "inject_brain_rules", "auto_correct", "session_close",
"pre_compact", "context_inject") so all BRAIN_DIR and quoting logic is
centralized.
🪄 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: 24fbaf65-efa6-4d1c-8081-9cbc845c6a88

📥 Commits

Reviewing files that changed from the base of the PR and between be4df94 and 52251e2.

📒 Files selected for processing (3)
  • Gradata/src/gradata/hooks/adapters/_base.py
  • Gradata/src/gradata/hooks/adapters/claude_code.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 ubuntu-latest / py3.11
  • GitHub Check: pytest ubuntu-latest / py3.12
  • GitHub Check: pytest macos-latest / py3.11
  • GitHub Check: pytest windows-latest / py3.12
  • GitHub Check: pytest macos-latest / py3.12
  • GitHub Check: pytest windows-latest / py3.11
  • GitHub Check: pytest (py3.12)
  • GitHub Check: pytest (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/claude_code.py
🔇 Additional comments (2)
Gradata/src/gradata/hooks/adapters/claude_code.py (1)

160-196: LGTM!

Gradata/tests/test_hook_adapters.py (1)

47-58: LGTM!

Also applies to: 61-76

Comment on lines +91 to +103
if not has_post_tool:
post_tool.append(
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": auto_correct_command(brain_dir),
"id": sig,
}
],
}
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Claude Code hooks matcher regex tool name matching exact or substring Edit MultiEdit

💡 Result:

In Claude Code, the matcher field in hook configurations determines which tool names trigger a specific hook [1][2]. Its behavior is determined by the content of the string provided [1]: 1. Exact Match or Pipe-Separated List: If the matcher string contains only letters, digits, underscores (_), or pipe characters (|), it is treated as an exact match or a list of specific tools [1][2]. For example, "Edit|MultiEdit|Write" will match any of those three tools exactly [3][4]. 2. Regular Expression: If the matcher string contains any character other than those listed above (such as a dot, caret, or asterisk), it is evaluated as a JavaScript regular expression [1][2]. For instance, "^Notebook" matches any tool name starting with "Notebook", and "mcp." matches any MCP tool [1][2]. When using tools like Edit and MultiEdit in hooks, you can group them using the pipe operator ("Edit|MultiEdit") to apply the same hook logic to both [3][4]. If you omit the matcher or set it to "" or "", the hook will fire for every occurrence of the event [1][2]. It is important to note that the matcher runs against the tool_name field provided in the JSON input to the hook [1][2]. Because non-regex matchers require exact matches, using patterns like "mcpmy_server" will fail to match "mcpmy_servertoolname"; in such cases, you must use a regex pattern like "mcpmy_server.*" to ensure a match [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Show relevant sections of the adapter
sed -n '1,220p' Gradata/src/gradata/hooks/adapters/claude_code.py | nl -ba | sed -n '1,220p' > /tmp/claude_code_head.txt
python3 - <<'PY'
import itertools, pathlib
path = pathlib.Path("Gradata/src/gradata/hooks/adapters/claude_code.py")
lines = path.read_text().splitlines()
def dump(start,end):
    for i in range(start-1,end):
        if 0<=i<len(lines):
            print(f"{i+1:4d}: {lines[i]}")
dump(1,140)
print("\n---\n")
dump(80,130)
print("\n---\n")
dump(20,60)
PY

Repository: Gradata/gradata

Length of output: 102


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import pathlib
path = pathlib.Path("Gradata/src/gradata/hooks/adapters/claude_code.py")
lines = path.read_text().splitlines()

def dump(start, end):
    for i in range(start-1, end):
        if 0 <= i < len(lines):
            print(f"{i+1:4d}: {lines[i]}")
print("== dump 1-140 ==")
dump(1, 140)
print("\n== dump 80-130 ==")
dump(80, 130)
print("\n== dump 20-60 ==")
dump(20, 60)
PY

Repository: Gradata/gradata

Length of output: 9335


PostToolUse matcher drops MultiEdit edits

In Gradata/src/gradata/hooks/adapters/claude_code.py (lines 91-103), the PostToolUse hook uses "matcher": "Edit|Write", so it triggers only for tool_name exactly Edit or Write. The adapter explicitly supports MultiEdit (EDIT_TOOLS includes "MultiEdit" and extract_correction() handles it), but the hook never fires for MultiEdit.

🐛 Proposed fix
-                    "matcher": "Edit|Write",
+                    "matcher": "Edit|MultiEdit|Write",
🤖 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 `@Gradata/src/gradata/hooks/adapters/claude_code.py` around lines 91 - 103, The
PostToolUse hook's matcher only matches "Edit" or "Write" so MultiEdit never
triggers; update the matcher used when building post_tool (variable post_tool in
claude_code.py) to include "MultiEdit" (e.g., change "matcher": "Edit|Write" to
include MultiEdit) so the hook fires for EDIT_TOOLS entries like "MultiEdit"
handled by extract_correction(); ensure the auto_correct_command(brain_dir) hook
entry (id: sig) remains unchanged.

Comment on lines +92 to +112
def test_claude_code_install_writes_pre_compact_entry(tmp_path: Path) -> None:
brain_dir = tmp_path / "brain"
brain_dir.mkdir()
config_path = tmp_path / ".claude" / "settings.json"

adapter = get_adapter("claude-code")
first = adapter.install(brain_dir, config_path)
second = adapter.install(brain_dir, config_path)

assert first.action == "added"
assert second.action == "already_present"
settings = json.loads(config_path.read_text(encoding="utf-8"))
pre_compact = settings["hooks"]["PreCompact"]
commands = [
hook.get("command", "")
for entry in pre_compact
for hook in entry.get("hooks", [])
]
assert len(pre_compact) == 1
assert any("BRAIN_DIR=" in command for command in commands)
assert any("gradata.hooks.pre_compact" in command for command in commands)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Extend coverage to the PostToolUse phase.

The test only asserts the PreCompact entry, yet the PR's core fix is wiring PostToolUse/auto_correct. Asserting the PostToolUse matcher and command would lock in the multi-phase contract and would have surfaced the MultiEdit matcher gap flagged in claude_code.py.

💚 Suggested additional assertions
     assert any("BRAIN_DIR=" in command for command in commands)
     assert any("gradata.hooks.pre_compact" in command for command in commands)
+
+    post_tool = settings["hooks"]["PostToolUse"]
+    assert len(post_tool) == 1
+    assert "MultiEdit" in post_tool[0]["matcher"]
+    post_commands = [
+        hook.get("command", "")
+        for entry in post_tool
+        for hook in entry.get("hooks", [])
+    ]
+    assert any("gradata.hooks.auto_correct" in command for command in post_commands)
🤖 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 `@Gradata/tests/test_hook_adapters.py` around lines 92 - 112, The test
test_claude_code_install_writes_pre_compact_entry currently only asserts the
PreCompact hook; extend it to also assert that a PostToolUse entry is written
and contains the expected matcher/command to cover the auto_correct wiring:
after loading settings = json.loads(config_path.read_text(...)) check
settings["hooks"]["PostToolUse"] exists, inspect its entries for the expected
matcher (e.g., "MultiEdit" / the matcher name used in claude_code.py) and that
one of its commands contains the PostToolUse-related module path (e.g.,
"gradata.hooks.post_tool_use" or the exact command string produced by
get_adapter("claude-code").install), mirroring the existing pattern used for
PreCompact to lock in the multi-phase contract.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant