Skip to content

Commit 1f79a36

Browse files
committed
refactor(hooks): centralize shared logic into utils.py and rename hooks
1 parent 5577295 commit 1f79a36

11 files changed

Lines changed: 320 additions & 182 deletions

File tree

.gemini/hooks/check_make.py

Lines changed: 0 additions & 39 deletions
This file was deleted.

.gemini/hooks/enforce_journal.py

Lines changed: 0 additions & 57 deletions
This file was deleted.

.gemini/hooks/journal.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Hook to enforce project journaling for all significant changes.
4+
5+
This hook is triggered 'AfterAgent' and checks if the daily journal entry has been updated
6+
whenever there are uncommitted changes in the repository.
7+
"""
8+
import sys
9+
import os
10+
from datetime import datetime
11+
12+
# Add the hooks directory to path for importing utils
13+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
14+
import utils
15+
16+
def main():
17+
"""
18+
Main entry point for the journal enforcement hook.
19+
20+
Checks git status for uncommitted changes and identifies if the current daily journal
21+
has been updated accordingly. Returns 'deny' if it's missing.
22+
"""
23+
try:
24+
modified_files = utils.get_modified_files()
25+
26+
today = datetime.now().strftime("%Y-%m-%d")
27+
journal_file = f"journal/{today}.md"
28+
29+
# Check if there are changes other than the daily journal
30+
significant_changes = [
31+
f for f in modified_files
32+
if f != journal_file
33+
]
34+
35+
# Check if the daily journal was updated
36+
journal_updated = journal_file in modified_files
37+
38+
if significant_changes and not journal_updated:
39+
utils.send_hook_decision(
40+
"deny",
41+
reason=(
42+
f"Please add a one-line entry to {journal_file} "
43+
"describing the changes you just made. Do not stop until this file is updated."
44+
)
45+
)
46+
else:
47+
utils.send_hook_decision("allow")
48+
49+
except Exception:
50+
# Failsafe: always allow if something goes wrong
51+
utils.send_hook_decision("allow")
52+
53+
if __name__ == "__main__":
54+
main()

.gemini/hooks/log.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Hook to log agent model output to the session log file for debugging and auditing.
4+
5+
This hook is triggered after the model response and processes JSON from stdin.
6+
The 'decision' is always 'allow'.
7+
"""
8+
import sys
9+
import os
10+
import json
11+
12+
# Add the hooks directory to path for importing utils
13+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
14+
import utils
15+
16+
def main():
17+
"""
18+
Main entry point for the log model output hook.
19+
20+
Reads candidate model responses from stdin and appends them to the log file.
21+
"""
22+
try:
23+
input_data = sys.stdin.read()
24+
if not input_data:
25+
utils.send_hook_decision("allow")
26+
return
27+
28+
data = json.loads(input_data)
29+
llm_response = data.get("llm_response", {})
30+
candidates = llm_response.get("candidates", [])
31+
32+
if candidates:
33+
candidate = candidates[0]
34+
content_obj = candidate.get("content", {})
35+
parts = content_obj.get("parts", [])
36+
finish_reason = candidate.get("finishReason")
37+
38+
if parts or finish_reason:
39+
content_to_log = ""
40+
for part in parts:
41+
if isinstance(part, str):
42+
content_to_log += part
43+
elif isinstance(part, dict) and "text" in part:
44+
content_to_log += part["text"]
45+
if finish_reason:
46+
content_to_log += "\n\n"
47+
48+
utils.log_message(content_to_log, mode="a")
49+
50+
utils.send_hook_decision("allow")
51+
52+
except Exception:
53+
# Failsafe: always allow if something goes wrong
54+
utils.send_hook_decision("allow")
55+
56+
if __name__ == "__main__":
57+
main()

.gemini/hooks/log_model_output.py

Lines changed: 0 additions & 42 deletions
This file was deleted.

.gemini/hooks/make.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Hook to run 'make' validation before critical agent actions.
4+
5+
This hook is triggered 'AfterAgent' and ensures that the codebase passes
6+
all linting and testing checks defined in the makefile.
7+
"""
8+
import sys
9+
import os
10+
import subprocess
11+
12+
# Add the hooks directory to path for importing utils
13+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
14+
import utils
15+
16+
def main():
17+
"""
18+
Main entry point for the make validation hook.
19+
20+
Executes 'uv run make' and returns a 'deny' decision if the process fails.
21+
"""
22+
try:
23+
# Run make command using uv run to ensure dependencies are available
24+
result = subprocess.run(
25+
["uv", "run", "make"],
26+
capture_output=True,
27+
text=True,
28+
check=False
29+
)
30+
31+
if result.returncode != 0:
32+
# make failed
33+
error_message = result.stdout + "\n" + result.stderr
34+
reason = (
35+
f"Validation failed (make returned {result.returncode}).\n"
36+
"Please fix the broken tests or linting issues.\n"
37+
"Output of 'make':\n"
38+
"```\n" + error_message.strip() + "\n```\n"
39+
"Fix these issues and ensure 'make' passes before continuing."
40+
)
41+
utils.send_hook_decision("deny", reason=reason)
42+
else:
43+
# make passed
44+
utils.send_hook_decision("allow")
45+
46+
except Exception:
47+
# Failsafe: always allow if the hook itself fails
48+
utils.send_hook_decision("allow")
49+
50+
if __name__ == "__main__":
51+
main()

.gemini/hooks/session.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Hook to initialize the Gemini CLI session by resetting the log file.
4+
5+
This hook is triggered on session 'startup', 'resume', or 'clear'.
6+
The 'decision' is always 'allow'.
7+
"""
8+
import sys
9+
import os
10+
from datetime import datetime
11+
12+
# Add the hooks directory to path for importing utils
13+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
14+
import utils
15+
16+
def main():
17+
"""
18+
Main entry point for the session start hook.
19+
20+
Resets the session log file with a timestamped entry.
21+
"""
22+
try:
23+
utils.log_message(
24+
f"--- Session Started ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')}) ---",
25+
mode="w"
26+
)
27+
utils.send_hook_decision("allow")
28+
except Exception:
29+
# Failsafe: always allow if something goes wrong
30+
utils.send_hook_decision("allow")
31+
32+
if __name__ == "__main__":
33+
main()

.gemini/hooks/session_start.py

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)