Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6cb259b
🧪 Add comprehensive tests for load_rule_file error handling
google-labs-jules[bot] Apr 10, 2026
954e3a2
Optimize backfill script with concurrent worker pool
google-labs-jules[bot] Apr 10, 2026
05396dd
Merge pull request #2 from Nietzsche-Ubermenschs/jules-74576980006059…
Nietzsche-Ubermensch Apr 10, 2026
36455a0
fix(scripts): refactor auto-close-duplicates to fix duplicate comment…
google-labs-jules[bot] Apr 10, 2026
50e813e
Optimize date comparisons and comment loop in auto-close-duplicates
google-labs-jules[bot] Apr 10, 2026
efa5bf9
Merge pull request #1 from Nietzsche-Ubermenschs/jules-79377771600588…
Nietzsche-Ubermensch Apr 10, 2026
ba09209
Merge pull request #4 from Nietzsche-Ubermenschs/jules-auto-close-dup…
Nietzsche-Ubermensch Apr 10, 2026
552dc16
Fix TODO in session-start.sh
google-labs-jules[bot] Apr 10, 2026
533a1b0
Fix logging and sanitize session_id in security_reminder_hook.py
google-labs-jules[bot] Apr 10, 2026
cd8de94
🧪 Add tests for extract_frontmatter markdown parser
google-labs-jules[bot] Apr 10, 2026
e12e995
⚡ Optimize hookify rule evaluation by skipping non-applicable rules
google-labs-jules[bot] Apr 10, 2026
9fa551b
Merge branch 'main' into fix-auto-close-duplicates-1978707039478511080
Nietzsche-Ubermensch Apr 10, 2026
5be4f79
Fix bug in security_reminder_hook.py
google-labs-jules[bot] Apr 10, 2026
2e96aa3
fix(security-guidance): Replace hardcoded debug log with standard log…
google-labs-jules[bot] Apr 10, 2026
b5099c9
Merge branch 'main' into test-config-loader-frontmatter-1608125145701…
Nietzsche-Ubermensch Apr 10, 2026
6f93269
Merge pull request #9 from Nietzsche-Ubermenschs/test-config-loader-f…
Nietzsche-Ubermensch Apr 10, 2026
16e567c
Merge pull request #3 from Nietzsche-Ubermenschs/fix-auto-close-dupli…
Nietzsche-Ubermensch Apr 10, 2026
30eb9f2
Merge pull request #12 from Nietzsche-Ubermenschs/jules-1524687401770…
Nietzsche-Ubermensch Apr 10, 2026
2b07069
Merge pull request #10 from Nietzsche-Ubermenschs/jules-1671717100203…
Nietzsche-Ubermensch Apr 10, 2026
a924f50
Merge branch 'main' into jules-4412982377496413932-36e56413
Nietzsche-Ubermensch Apr 10, 2026
0c148e9
Merge pull request #8 from Nietzsche-Ubermenschs/jules-44129823774964…
Nietzsche-Ubermensch Apr 10, 2026
df293d5
Merge branch 'main' into fix/security-reminder-logging-95730501940379…
Nietzsche-Ubermensch Apr 10, 2026
af4ee0f
Merge pull request #13 from Nietzsche-Ubermenschs/fix/security-remind…
Nietzsche-Ubermensch Apr 10, 2026
8c69865
Merge pull request #7 from Nietzsche-Ubermenschs/fix-todo-session-sta…
Nietzsche-Ubermensch Apr 10, 2026
087c19e
fix(scripts): Fix multiline template literals in auto-close-duplicate…
google-labs-jules[bot] Apr 10, 2026
808962e
perf(hookify): index rules by event type in evaluate_rules (#18)
google-labs-jules[bot] Apr 10, 2026
8390e63
fix: ignore pull requests in backfill-duplicate-comments.ts (#15)
google-labs-jules[bot] Apr 10, 2026
2ff87af
Optimize date comparison and array iteration in auto-close-duplicates…
google-labs-jules[bot] Apr 10, 2026
43d230e
Perf: Optimize API calls with concurrency in auto-close-duplicates (#17)
google-labs-jules[bot] Apr 10, 2026
e16ff44
⚡ Optimize security reminder hook substring checking (#21)
google-labs-jules[bot] Apr 10, 2026
9744805
Optimize auto-close-duplicates with concurrency and fast date checks …
google-labs-jules[bot] Apr 10, 2026
9882836
Merge branch 'anthropics:main' into main
Nietzsche-Ubermensch Apr 10, 2026
b58ad81
🧪 Add tests for check_patterns security analysis (#20)
google-labs-jules[bot] Apr 10, 2026
21ae6e9
⚡ Optimize file I/O using os.scandir in security_reminder_hook.py (#19)
google-labs-jules[bot] Apr 10, 2026
1c23f08
Fix TODO false positive in setup-ralph-loop.sh (#11)
google-labs-jules[bot] Apr 10, 2026
a5f4b4f
🧪 [Testing] Add unit tests for githubRequest API wrapper (#14)
google-labs-jules[bot] Apr 10, 2026
e6fc884
Add CodeQL analysis workflow
Nietzsche-Ubermensch Apr 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '35 21 * * 3'

jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write

# required to fetch internal or private CodeQL packs
packages: read

# only required for workflows in private repositories
actions: read
contents: read

strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
- language: python
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4

# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.

# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality

# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
64 changes: 63 additions & 1 deletion plugins/hookify/core/rule_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,65 @@ def evaluate_rules(self, rules: List[Rule], input_data: Dict[str, Any]) -> Dict[
Empty dict {} if no rules match.
"""
hook_event = input_data.get('hook_event_name', '')
tool_name = input_data.get('tool_name', '')
blocking_rules = []
warning_rules = []

# Determine the event category based on the hook event and tool
event_category = None
if hook_event in ['PreToolUse', 'PostToolUse']:
if tool_name == 'Bash':
event_category = 'bash'
elif tool_name in ['Edit', 'Write', 'MultiEdit']:
event_category = 'file'
elif hook_event == 'Stop':
event_category = 'stop'
elif hook_event == 'UserPromptSubmit':
event_category = 'prompt'

# Build an index if rules object has changed or we don't have one
rules_id = id(rules)
if getattr(self, '_rules_cache_id', None) != rules_id:
self._rules_cache_id = rules_id
self._rules_by_event = {}
for rule in rules:
rule_event = getattr(rule, 'event', 'all')
if rule_event not in self._rules_by_event:
self._rules_by_event[rule_event] = []
self._rules_by_event[rule_event].append(rule)

# Get relevant rules: those specifically for this event category, plus "all" rules
relevant_rules = []
if event_category and event_category in self._rules_by_event:
relevant_rules.extend(self._rules_by_event[event_category])
if 'all' in self._rules_by_event:
relevant_rules.extend(self._rules_by_event['all'])

# If no event category could be determined, evaluate all rules to be safe
if not event_category:
relevant_rules = rules

for rule in relevant_rules:

# Determine internal event type for filtering
event_type = None
if hook_event == 'Stop':
event_type = 'stop'
elif hook_event == 'UserPromptSubmit':
event_type = 'prompt'
elif tool_name == 'Bash':
event_type = 'bash'
elif tool_name in ['Edit', 'Write', 'MultiEdit']:
event_type = 'file'

blocking_rules = []
warning_rules = []

for rule in rules:
# Skip rules that don't match the current event type
if event_type and rule.event not in (event_type, 'all'):
continue

if self._rule_matches(rule, input_data):
if rule.action == 'block':
blocking_rules.append(rule)
Expand Down Expand Up @@ -205,12 +260,19 @@ def _extract_field(self, field: str, tool_name: str,
if field == 'reason':
return input_data.get('reason', '')
elif field == 'transcript':
# Check if we've already cached the transcript in this evaluation
if '_transcript_content' in input_data:
return input_data['_transcript_content']

# Read transcript file if path provided
transcript_path = input_data.get('transcript_path')
if transcript_path:
try:
with open(transcript_path, 'r') as f:
return f.read()
content = f.read()
# Cache it in input_data to prevent redundant disk I/O per memory instructions
input_data['_transcript_content'] = content
return content
except FileNotFoundError:
print(f"Warning: Transcript file not found: {transcript_path}", file=sys.stderr)
return ''
Expand Down
206 changes: 206 additions & 0 deletions plugins/hookify/core/test_config_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import pytest
import sys
import os

# Add plugins/ directory to path so we can import hookify
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))

from hookify.core.config_loader import extract_frontmatter

def test_missing_frontmatter():
content = "Hello, world!\nThis is a test."
fm, msg = extract_frontmatter(content)
assert fm == {}
assert msg == content

def test_simple_key_value():
content = """---
name: test-rule
enabled: true
event: bash
---
Message body
"""
fm, msg = extract_frontmatter(content)
assert fm == {"name": "test-rule", "enabled": True, "event": "bash"}
assert msg == "Message body"

def test_boolean_conversion():
content = """---
is_true: true
is_false: false
is_True: True
is_False: False
is_string: "true"
---
Message
"""
fm, msg = extract_frontmatter(content)
assert fm["is_true"] is True
assert fm["is_false"] is False
assert fm["is_True"] is True
assert fm["is_False"] is False
assert fm["is_string"] is True

def test_list_items():
content = """---
events:
- bash
- file
- stop
---
Message
"""
fm, msg = extract_frontmatter(content)
assert fm == {"events": ["bash", "file", "stop"]}
assert msg == "Message"

def test_inline_dict_in_list():
content = """---
conditions:
- field: command, operator: regex_match, pattern: rm -rf
- field: command, operator: contains, pattern: sudo
---
Message
"""
fm, msg = extract_frontmatter(content)
assert fm == {
"conditions": [
{"field": "command", "operator": "regex_match", "pattern": "rm -rf"},
{"field": "command", "operator": "contains", "pattern": "sudo"}
]
}
assert msg == "Message"

def test_multiline_dict_in_list():
content = """---
conditions:
- field: command
operator: regex_match
pattern: "rm -rf"
- field: command
operator: contains
pattern: sudo
---
Message
"""
fm, msg = extract_frontmatter(content)
assert fm == {
"conditions": [
{"field": "command", "operator": "regex_match", "pattern": "rm -rf"},
{"field": "command", "operator": "contains", "pattern": "sudo"}
]
}
assert msg == "Message"

def test_comments_and_blank_lines():
content = """---
# This is a comment
name: test-rule

# Another comment
enabled: true
---
Message
"""
fm, msg = extract_frontmatter(content)
assert fm == {"name": "test-rule", "enabled": True}
assert msg == "Message"

def test_complex_frontmatter():
content = """---
name: require-tests
enabled: true
event: bash
conditions:
- field: command
operator: regex_match
pattern: "git commit"
- field: command
operator: regex_match
pattern: "-m"
action: warn
---
You are about to commit code!
Make sure you run the tests first!
"""
fm, msg = extract_frontmatter(content)
assert fm["name"] == "require-tests"
assert fm["enabled"] is True
assert fm["event"] == "bash"
assert len(fm["conditions"]) == 2
assert fm["conditions"][0] == {"field": "command", "operator": "regex_match", "pattern": "git commit"}
assert fm["conditions"][1] == {"field": "command", "operator": "regex_match", "pattern": "-m"}
assert fm["action"] == "warn"
assert msg == "You are about to commit code!\nMake sure you run the tests first!"
import os
import sys
from unittest.mock import mock_open, patch

# Make sure we can import plugins.hookify...
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..')))

from plugins.hookify.core.config_loader import load_rule_file

def test_load_rule_file_success():
"""Test successful loading of a rule file."""
content = """---
name: test_rule
enabled: true
event: bash
---

Test message"""
with patch('builtins.open', mock_open(read_data=content)):
rule = load_rule_file('dummy.md')
assert rule is not None
assert rule.name == 'test_rule'
assert rule.enabled is True
assert rule.event == 'bash'
assert rule.message == 'Test message'

def test_load_rule_file_ioerror():
"""Test loading a rule file that doesn't exist."""
with patch('builtins.open', side_effect=IOError("File not found")):
rule = load_rule_file('nonexistent.md')
assert rule is None

def test_load_rule_file_permission_error():
"""Test loading a rule file with permission error."""
with patch('builtins.open', side_effect=PermissionError("Permission denied")):
rule = load_rule_file('noperms.md')
assert rule is None

def test_load_rule_file_oserror():
"""Test loading a rule file with generic OSError."""
with patch('builtins.open', side_effect=OSError("Generic OS error")):
rule = load_rule_file('oserror.md')
assert rule is None

def test_load_rule_file_missing_frontmatter():
"""Test loading a rule file missing frontmatter."""
content = "Just some text without frontmatter"
with patch('builtins.open', mock_open(read_data=content)):
rule = load_rule_file('no_frontmatter.md')
assert rule is None

def test_load_rule_file_malformed_frontmatter():
"""Test loading a rule file with malformed frontmatter (ValueError from parser)."""
# Force extract_frontmatter to raise ValueError
with patch('builtins.open', mock_open(read_data="---\nfoo\n---")), \
patch('plugins.hookify.core.config_loader.extract_frontmatter', side_effect=ValueError("Malformed")):
rule = load_rule_file('malformed.md')
assert rule is None

def test_load_rule_file_unicode_decode_error():
"""Test loading a rule file with invalid encoding."""
with patch('builtins.open', mock_open()) as m_open:
m_open.return_value.read.side_effect = UnicodeDecodeError('utf-8', b'\\x80', 0, 1, 'invalid start byte')
rule = load_rule_file('bad_encoding.md')
assert rule is None

def test_load_rule_file_unexpected_exception():
"""Test loading a rule file throwing an unexpected exception."""
with patch('builtins.open', side_effect=Exception("Unexpected")):
rule = load_rule_file('unexpected.md')
assert rule is None
Loading