Compare commits

..

4 Commits

Author SHA1 Message Date
cbfb6ae514 fix: add PR template — reviewer checklist (#1558)
Some checks failed
Check PR Changes / check-changes (pull_request) Successful in 16s
CI / test (pull_request) Failing after 1m16s
CI / validate (pull_request) Failing after 1m14s
Review Approval Gate / verify-review (pull_request) Successful in 11s
2026-04-15 03:46:45 +00:00
098fe746d7 fix: add docs/rubber-stamping-prevention.md — prevent rubber-stamping (#1558) 2026-04-15 03:45:16 +00:00
23b04b50eb fix: add bin/check_zombie_prs.py — prevent rubber-stamping (#1558) 2026-04-15 03:45:14 +00:00
205252f048 fix: add .gitea/workflows/check-pr-changes.yml — prevent rubber-stamping (#1558) 2026-04-15 03:45:12 +00:00
7 changed files with 236 additions and 515 deletions

View File

@@ -0,0 +1,23 @@
## Description
<!-- What does this PR do? -->
## Changes
- [ ]
## Testing
- [ ]
## Reviewer Checklist
**IMPORTANT: Do not rubber-stamp. Verify each item below.**
- [ ] **PR has actual changes** — check additions, deletions, and changed files are > 0
- [ ] **Changes match description** — the code changes match what the PR claims to do
- [ ] **Code quality** — no obvious bugs, follows conventions, readable
- [ ] **Tests are adequate** — new code has tests, existing tests pass
- [ ] **Documentation updated** — if applicable
**By approving, I confirm I have actually reviewed the code changes in this PR.**

View File

@@ -0,0 +1,40 @@
name: Check PR Changes
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
check-changes:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for actual changes
run: |
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
ADDITIONS=${{ github.event.pull_request.additions }}
DELETIONS=${{ github.event.pull_request.deletions }}
CHANGED_FILES=${{ github.event.pull_request.changed_files }}
echo "PR Stats: +${ADDITIONS} -${DELETIONS} files:${CHANGED_FILES}"
if [ "$ADDITIONS" -eq 0 ] && [ "$DELETIONS" -eq 0 ] && [ "$CHANGED_FILES" -eq 0 ]; then
echo "::error::ZOMBIE PR detected — zero changes between base and head."
echo "This PR has no additions, deletions, or changed files."
echo "Please add actual changes or close this PR."
exit 1
fi
# Check for empty commits
COMMITS=$(git rev-list --count "$BASE".."$HEAD" 2>/dev/null || echo "0")
if [ "$COMMITS" -eq 0 ]; then
echo "::warning::PR has no commits between base and head."
fi
echo "PR has valid changes (+${ADDITIONS} -${DELETIONS})."

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env bash
# Commit-msg hook: warn about shell injection risks
# Install: cp .githooks/commit-msg .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg
COMMIT_MSG_FILE="$1"
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# Check for dangerous patterns
DANGEROUS_PATTERNS=(
'`' # Backticks
'$(' # Command substitution
'${' # Variable expansion
'\\`' # Escaped backticks
'eval ' # eval command
'exec ' # exec command
'source ' # source command
'|' # Pipe
'&&' # AND operator
'||' # OR operator
';' # Semicolon
'>' # Redirect
'<' # Input redirect
)
FOUND_ISSUES=()
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMIT_MSG" | grep -q "$pattern"; then
FOUND_ISSUES+=("$pattern")
fi
done
if [ ${#FOUND_ISSUES[@]} -gt 0 ]; then
echo "⚠️ WARNING: Commit message contains potentially dangerous patterns:"
for issue in "${FOUND_ISSUES[@]}"; do
echo " - $issue"
done
echo ""
echo "This could trigger shell execution during git operations."
echo ""
echo "Safe alternatives:"
echo " 1. Use: git commit -F <file> instead of git commit -m"
echo " 2. Escape special characters in commit messages"
echo " 3. Use the safe_commit() function from bin/safe_commit.py"
echo ""
echo "To proceed anyway, use: git commit --no-verify"
exit 1
fi
exit 0

121
bin/check_zombie_prs.py Normal file
View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
Zombie PR Detector — scans Gitea repos for PRs with no changes.
Usage:
python bin/check_zombie_prs.py
python bin/check_zombie_prs.py --repos the-nexus timmy-home
python bin/check_zombie_prs.py --report
"""
import argparse
import json
import os
import urllib.request
from typing import Optional
def get_token() -> str:
"""Read Gitea API token."""
for path in ["~/.config/gitea/token", "~/.config/forge.token"]:
expanded = os.path.expanduser(path)
if os.path.exists(expanded):
return open(expanded).read().strip()
raise RuntimeError("No Gitea token found")
def get_open_prs(token: str, repo: str, base_url: str) -> list:
"""Get all open PRs for a repo."""
url = f"{base_url}/repos/{repo}/pulls?state=open&limit=100"
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
return json.loads(urllib.request.urlopen(req, timeout=30).read())
def check_pr_zombie(pr: dict) -> Optional[dict]:
"""Check if a PR is a zombie (no changes)."""
additions = pr.get("additions", 0)
deletions = pr.get("deletions", 0)
changed_files = pr.get("changed_files", 0)
if additions == 0 and deletions == 0 and changed_files == 0:
return {
"number": pr["number"],
"title": pr["title"],
"author": pr.get("user", {}).get("login", "unknown"),
"url": pr.get("html_url", ""),
"created": pr.get("created_at", ""),
"additions": additions,
"deletions": deletions,
"changed_files": changed_files,
}
return None
def scan_repos(token: str, repos: list, base_url: str) -> list:
"""Scan repos for zombie PRs."""
zombies = []
for repo in repos:
try:
prs = get_open_prs(token, repo, base_url)
for pr in prs:
zombie = check_pr_zombie(pr)
if zombie:
zombie["repo"] = repo
zombies.append(zombie)
except Exception as e:
print(f" Error scanning {repo}: {e}")
return zombies
def list_org_repos(token: str, org: str, base_url: str) -> list:
"""List all repos in an org."""
url = f"{base_url}/orgs/{org}/repos?limit=100"
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
repos = json.loads(urllib.request.urlopen(req, timeout=30).read())
return [r["full_name"] for r in repos]
def main():
parser = argparse.ArgumentParser(description="Detect zombie PRs with no changes")
parser.add_argument("--repos", nargs="+", help="Specific repos to scan")
parser.add_argument("--org", default="Timmy_Foundation", help="Organization name")
parser.add_argument("--base-url", default="https://forge.alexanderwhitestone.com/api/v1")
parser.add_argument("--report", action="store_true", help="Generate detailed report")
args = parser.parse_args()
token = get_token()
if args.repos:
repos = [f"{args.org}/{r}" if "/" not in r else r for r in args.repos]
else:
repos = list_org_repos(token, args.org, args.base_url)
print(f"Scanning {len(repos)} repos...")
zombies = scan_repos(token, repos, args.base_url)
if zombies:
print(f"\nFOUND {len(zombies)} ZOMBIE PR(s):\n")
for z in zombies:
print(f" [{z['repo']}] #{z['number']}: {z['title']}")
print(f" Author: {z['author']} Created: {z['created']}")
print(f" Stats: +{z['additions']} -{z['deletions']} files:{z['changed_files']}")
print(f" URL: {z['url']}")
print()
else:
print("\nNo zombie PRs found. All clear.")
if args.report:
report = {
"scanned_repos": len(repos),
"zombie_prs": len(zombies),
"zombies": zombies,
}
report_path = os.path.expanduser("~/.hermes/reports/zombie_prs.json")
os.makedirs(os.path.dirname(report_path), exist_ok=True)
with open(report_path, "w") as f:
json.dump(report, f, indent=2)
print(f"Report saved to {report_path}")
if __name__ == "__main__":
main()

View File

@@ -1,307 +0,0 @@
#!/usr/bin/env python3
"""
Safe commit message handler to prevent shell injection.
Issue #1430: [IMPROVEMENT] memory_mine.py ran during git commit — shell injection from commit message
This script provides safe ways to commit with code-containing messages.
"""
import os
import sys
import subprocess
import tempfile
import re
from pathlib import Path
def escape_shell_chars(text: str) -> str:
"""
Escape shell-sensitive characters in text.
This prevents shell injection when text is used in shell commands.
"""
# Characters that need escaping in shell
shell_chars = ['$', '`', '\\', '"', "'", '!', '(', ')', '{', '}', '[', ']',
'|', '&', ';', '<', '>', '*', '?', '~', '#']
escaped = text
for char in shell_chars:
escaped = escaped.replace(char, '\\' + char)
return escaped
def safe_commit_message(message: str) -> str:
"""
Create a safe commit message by escaping shell-sensitive characters.
Args:
message: The commit message
Returns:
Escaped commit message safe for shell use
"""
return escape_shell_chars(message)
def commit_with_file(message: str, branch: str = None) -> bool:
"""
Commit using a temporary file instead of -m flag.
This is the safest way to commit messages containing code or special characters.
Args:
message: The commit message
branch: Optional branch name
Returns:
True if successful, False otherwise
"""
# Create temporary file for commit message
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
f.write(message)
temp_file = f.name
try:
# Build git command
cmd = ['git', 'commit', '-F', temp_file]
if branch:
cmd.extend(['-b', branch])
# Execute git commit
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f"✅ Committed successfully using file: {temp_file}")
return True
else:
print(f"❌ Commit failed: {result.stderr}")
return False
finally:
# Clean up temporary file
try:
os.unlink(temp_file)
except:
pass
def commit_safe(message: str, use_file: bool = True) -> bool:
"""
Safely commit with a message.
Args:
message: The commit message
use_file: If True, use -F <file> instead of -m
Returns:
True if successful, False otherwise
"""
if use_file:
return commit_with_file(message)
else:
# Use escaped message with -m flag
escaped_message = safe_commit_message(message)
cmd = ['git', 'commit', '-m', escaped_message]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print("✅ Committed successfully with escaped message")
return True
else:
print(f"❌ Commit failed: {result.stderr}")
return False
def check_commit_message_safety(message: str) -> dict:
"""
Check if a commit message contains potentially dangerous patterns.
Args:
message: The commit message to check
Returns:
Dictionary with safety analysis
"""
dangerous_patterns = [
(r'`[^`]*`', 'Backticks (shell command substitution)'),
(r'\$\([^)]*\)', 'Command substitution $(...)'),
(r'\$\{[^}]*\}', 'Variable expansion ${...}'),
(r'\\`', 'Escaped backticks'),
(r'eval\s+', 'eval command'),
(r'exec\s+', 'exec command'),
(r'source\s+', 'source command'),
(r'\.\s+', 'dot command'),
(r'\|\s*', 'Pipe character'),
(r'&&', 'AND operator'),
(r'\|\|', 'OR operator'),
(r';', 'Semicolon (command separator)'),
(r'>', 'Redirect operator'),
(r'<', 'Input redirect'),
]
findings = []
for pattern, description in dangerous_patterns:
matches = re.findall(pattern, message)
if matches:
findings.append({
'pattern': pattern,
'description': description,
'matches': matches,
'count': len(matches)
})
return {
'safe': len(findings) == 0,
'findings': findings,
'recommendation': 'Use commit_with_file() or escape_shell_chars()' if findings else 'Message appears safe'
}
def create_commit_hook_guard():
"""
Create a commit-msg hook that warns about dangerous patterns.
"""
hook_content = '''#!/usr/bin/env bash
# Commit-msg hook: warn about shell injection risks
# Install: cp .githooks/commit-msg .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg
COMMIT_MSG_FILE="$1"
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# Check for dangerous patterns
DANGEROUS_PATTERNS=(
'`' # Backticks
'$(' # Command substitution
'${' # Variable expansion
'\\`' # Escaped backticks
'eval ' # eval command
'exec ' # exec command
'source ' # source command
'|' # Pipe
'&&' # AND operator
'||' # OR operator
';' # Semicolon
'>' # Redirect
'<' # Input redirect
)
FOUND_ISSUES=()
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMIT_MSG" | grep -q "$pattern"; then
FOUND_ISSUES+=("$pattern")
fi
done
if [ ${#FOUND_ISSUES[@]} -gt 0 ]; then
echo "⚠️ WARNING: Commit message contains potentially dangerous patterns:"
for issue in "${FOUND_ISSUES[@]}"; do
echo " - $issue"
done
echo ""
echo "This could trigger shell execution during git operations."
echo ""
echo "Safe alternatives:"
echo " 1. Use: git commit -F <file> instead of git commit -m"
echo " 2. Escape special characters in commit messages"
echo " 3. Use the safe_commit() function from bin/safe_commit.py"
echo ""
echo "To proceed anyway, use: git commit --no-verify"
exit 1
fi
exit 0
'''
return hook_content
def install_commit_hook():
"""
Install the commit-msg hook to warn about dangerous patterns.
"""
hook_path = Path('.git/hooks/commit-msg')
hook_content = create_commit_hook_guard()
# Check if .git/hooks exists
if not hook_path.parent.exists():
print("❌ .git/hooks directory not found")
return False
# Write hook
with open(hook_path, 'w') as f:
f.write(hook_content)
# Make executable
os.chmod(hook_path, 0o755)
print(f"✅ Installed commit-msg hook to {hook_path}")
return True
def main():
"""Main entry point for safe commit tool."""
import argparse
parser = argparse.ArgumentParser(description="Safe commit message handling")
parser.add_argument("--message", "-m", help="Commit message")
parser.add_argument("--file", "-F", help="Read commit message from file")
parser.add_argument("--check", action="store_true", help="Check message safety")
parser.add_argument("--install-hook", action="store_true", help="Install commit-msg hook")
parser.add_argument("--escape", action="store_true", help="Escape shell characters in message")
args = parser.parse_args()
if args.install_hook:
if install_commit_hook():
print("Commit hook installed successfully")
else:
print("Failed to install commit hook")
sys.exit(1)
return
if args.check:
if args.message:
safety = check_commit_message_safety(args.message)
print(f"Message safety check:")
print(f" Safe: {safety['safe']}")
print(f" Recommendation: {safety['recommendation']}")
if safety['findings']:
print(f" Findings:")
for finding in safety['findings']:
print(f" - {finding['description']}: {finding['count']} matches")
else:
print("Please provide a message with --message")
return
if args.escape:
if args.message:
escaped = safe_commit_message(args.message)
print(f"Escaped message:")
print(escaped)
else:
print("Please provide a message with --message")
return
if args.file:
# Read message from file
with open(args.file, 'r') as f:
message = f.read()
commit_with_file(message)
elif args.message:
# Check if message has dangerous patterns
safety = check_commit_message_safety(args.message)
if safety['safe']:
commit_safe(args.message, use_file=False)
else:
print("⚠️ Message contains potentially dangerous patterns")
print("Using file-based commit for safety...")
commit_safe(args.message, use_file=True)
else:
parser.print_help()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,52 @@
# Rubber-Stamping Prevention
## What is Rubber-Stamping?
Rubber-stamping is approving a PR without actually reviewing the code. This was observed in PR #359 which received 3 APPROVED reviews despite having zero changes.
## Why It's Bad
1. Wastes reviewer time
2. Creates false sense of review quality
3. Allows zombie PRs to appear reviewed
## Prevention Measures
### 1. CI Check (`.gitea/workflows/check-pr-changes.yml`)
Automated check that runs on every PR:
- Detects PRs with no changes (0 additions, 0 deletions, 0 files changed)
- Blocks merge if PR is a zombie
- Provides clear error messages
### 2. PR Template
Enhanced reviewer checklist:
- Verify PR has actual changes
- Changes match description
- Code quality review
- Tests are adequate
- Documentation is updated
### 3. Zombie PR Detection
```bash
# Scan all repos
python bin/check_zombie_prs.py
# Scan specific repos
python bin/check_zombie_prs.py --repos the-nexus timmy-home
# Generate report
python bin/check_zombie_prs.py --report
```
## Testing
```bash
# Create a test PR with no changes
git checkout -b test/zombie-pr
git commit --allow-empty -m "test: empty commit"
git push origin test/zombie-pr
# Create PR — CI should fail
```

View File

@@ -1,159 +0,0 @@
# Safe Commit Practices
**Issue:** #1430 - [IMPROVEMENT] memory_mine.py ran during git commit — shell injection from commit message
## Problem
During commit for #1124, the commit message contained Python code examples that triggered shell execution of memory_mine.py. The backtick-wrapped code in the commit message was interpreted by the shell during git commit processing.
This is a potential vector for unintended code execution.
## Safe Commit Methods
### 1. Use `git commit -F <file>` (Recommended)
The safest way to commit messages containing code or special characters:
```bash
# Create a file with your commit message
echo "Fix: implement memory_mine.py with backtick example
Example: \`python3 bin/memory_mine.py --days 7\`
This commit adds memory mining functionality." > /tmp/commit-msg.txt
# Commit using the file
git commit -F /tmp/commit-msg.txt
```
### 2. Use the Safe Commit Tool
```bash
# Safe commit with automatic escaping
python3 bin/safe_commit.py -m "Fix: implement memory_mine.py with backtick example"
# Safe commit using file
python3 bin/safe_commit.py -F /tmp/commit-msg.txt
# Check if a message is safe
python3 bin/safe_commit.py --check -m "Example: \`python3 bin/memory_mine.py\`"
```
### 3. Escape Shell Characters Manually
If you must use `git commit -m`, escape special characters:
```bash
# Escape backticks and other shell characters
git commit -m "Fix: implement memory_mine.py with backtick example
Example: \\`python3 bin/memory_mine.py --days 7\\`
This commit adds memory mining functionality."
```
## Dangerous Patterns to Avoid
The following patterns in commit messages can trigger shell execution:
- **Backticks**: `` `command` `` → Executes command
- **Command substitution**: `$(command)` → Executes command
- **Variable expansion**: `${variable}` → Expands variable
- **Pipes**: `command1 | command2` → Pipes output
- **Operators**: `&&`, `||`, `;` → Command chaining
- **Redirects**: `>`, `<` → File operations
## Installation
### Install the Commit Hook
To automatically warn about dangerous patterns:
```bash
# Install the commit-msg hook
python3 bin/safe_commit.py --install-hook
# Or manually
cp .githooks/commit-msg .git/hooks/commit-msg
chmod +x .git/hooks/commit-msg
```
### Configure Git Hooks Path
If using the `.githooks` directory:
```bash
git config core.hooksPath .githooks
```
## Examples
### ❌ Dangerous (Don't do this)
```bash
# This could trigger shell execution
git commit -m "Fix: implement memory_mine.py
Example: \`python3 bin/memory_mine.py --days 7\`
This mines sessions into MemPalace."
```
### ✅ Safe (Do this instead)
```bash
# Method 1: Use file
echo "Fix: implement memory_mine.py
Example: \`python3 bin/memory_mine.py --days 7\`
This mines sessions into MemPalace." > /tmp/commit-msg.txt
git commit -F /tmp/commit-msg.txt
# Method 2: Use safe commit tool
python3 bin/safe_commit.py -m "Fix: implement memory_mine.py
Example: \`python3 bin/memory_mine.py --days 7\`
This mines sessions into MemPalace."
# Method 3: Escape manually
git commit -m "Fix: implement memory_mine.py
Example: \\`python3 bin/memory_mine.py --days 7\\`
This mines sessions into MemPalace."
```
## What Happened in Issue #1430
During commit for #1124, a commit message contained:
```
Example: \`python3 bin/memory_mine.py --days 7\`
```
The backticks were interpreted by the shell during git commit processing, causing memory_mine.py to execute. While the outcome was positive (26 sessions mined), this is a security risk.
## Prevention
1. **Always use `git commit -F <file>`** for messages containing code
2. **Install the commit-msg hook** to warn about dangerous patterns
3. **Use the safe_commit.py tool** for automatic escaping
4. **Document safe patterns** in team guidelines
## Related Issues
- **Issue #1430:** This improvement
- **Issue #1124:** Original issue that triggered the problem
## Files
- `bin/safe_commit.py` - Safe commit tool
- `.githooks/commit-msg` - Commit hook (to be installed)
- `docs/safe-commit-practices.md` - This documentation
## Conclusion
Shell injection in commit messages is a real security risk. By using safe commit practices, we can prevent unintended code execution while still allowing code examples in commit messages.
**Remember:** When in doubt, use `git commit -F <file>` instead of `git commit -m`.