-
Notifications
You must be signed in to change notification settings - Fork 53
feat: support Husky as equivalent pre-commit hook framework #467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -588,9 +588,19 @@ def assess(self, repository: Repository) -> Finding: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score += 10.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| evidence.append(".claude/settings.json exists") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if husky_dir.exists(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score += 10.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| evidence.append(".husky directory found (git hooks)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if husky_dir.exists() and husky_dir.is_dir(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hook_scripts = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f.name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for f in husky_dir.iterdir() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if f.is_file() and not f.name.startswith("_") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if hook_scripts: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score += 60.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hooks_list = ", ".join(sorted(hook_scripts)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| evidence.append(f".husky directory found with hooks: {hooks_list}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+592
to
+600
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don’t treat arbitrary files in Current detection counts any regular file (except underscore-prefixed), so files like Suggested fix+ valid_hook_names = {
+ "applypatch-msg",
+ "commit-msg",
+ "post-applypatch",
+ "post-checkout",
+ "post-commit",
+ "post-merge",
+ "post-rewrite",
+ "pre-applypatch",
+ "pre-auto-gc",
+ "pre-commit",
+ "pre-merge-commit",
+ "pre-push",
+ "pre-rebase",
+ "prepare-commit-msg",
+ }
try:
hook_scripts = [
f.name
for f in husky_dir.iterdir()
- if f.is_file() and not f.name.startswith("_")
+ if f.is_file()
+ and not f.name.startswith("_")
+ and f.name in valid_hook_names
]📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score += 10.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| evidence.append(".husky directory found but no hook scripts") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score = min(score, 100.0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -634,14 +644,14 @@ def _create_remediation(self) -> Remediation: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| summary="Set up deterministic enforcement with hooks and lint rules", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Start with 2 hooks: auto-format on edit + block destructive operations", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Install pre-commit framework for git hooks", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Install pre-commit (Python) or Husky (Node.js) for git hooks", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Configure .claude/settings.json with agent hooks for team-wide sharing", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Add lint rules for import restrictions and architectural boundaries", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tools=["pre-commit"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tools=["pre-commit", "husky"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| commands=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "pip install pre-commit", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "pre-commit install", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "pip install pre-commit && pre-commit install", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "npx husky init", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "mkdir -p .claude", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| examples=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -561,6 +561,49 @@ def test_pre_commit_config_passes(self, tmp_path): | |
| assert finding.score >= 60.0 | ||
| assert any("pre-commit" in e.lower() for e in finding.evidence) | ||
|
|
||
| def test_husky_with_hooks_passes(self, tmp_path): | ||
| """Test that .husky directory with hook scripts passes.""" | ||
| husky_dir = tmp_path / ".husky" | ||
| husky_dir.mkdir() | ||
| (husky_dir / "pre-commit").write_text("#!/bin/sh\nnpx lint-staged\n") | ||
| (husky_dir / "commit-msg").write_text("#!/bin/sh\nnpx commitlint --edit $1\n") | ||
|
|
||
| repo = _make_repo(tmp_path) | ||
| assessor = DeterministicEnforcementAssessor() | ||
| finding = assessor.assess(repo) | ||
|
|
||
| assert finding.status == "pass" | ||
| assert finding.score >= 60.0 | ||
| assert any(".husky" in e for e in finding.evidence) | ||
| assert any("pre-commit" in e for e in finding.evidence) | ||
| assert any("commit-msg" in e for e in finding.evidence) | ||
|
|
||
| def test_husky_empty_dir_does_not_pass(self, tmp_path): | ||
| """Test that .husky directory without hook scripts only gives partial score.""" | ||
| husky_dir = tmp_path / ".husky" | ||
| husky_dir.mkdir() | ||
|
|
||
| repo = _make_repo(tmp_path) | ||
| assessor = DeterministicEnforcementAssessor() | ||
| finding = assessor.assess(repo) | ||
|
|
||
| assert finding.status == "fail" | ||
| assert finding.score == 10.0 | ||
| assert any("no hook scripts" in e for e in finding.evidence) | ||
|
|
||
| def test_husky_ignores_underscore_files(self, tmp_path): | ||
| """Test that files starting with _ (like _/husky.sh) are ignored.""" | ||
| husky_dir = tmp_path / ".husky" | ||
| husky_dir.mkdir() | ||
| (husky_dir / "_").mkdir() | ||
| (husky_dir / "pre-commit").write_text("#!/bin/sh\nnpx lint-staged\n") | ||
|
|
||
| repo = _make_repo(tmp_path) | ||
| assessor = DeterministicEnforcementAssessor() | ||
| finding = assessor.assess(repo) | ||
|
|
||
| assert finding.score >= 60.0 | ||
|
|
||
|
Comment on lines
+594
to
+606
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This test uses an underscore directory ( As per coding guidelines, "tests/**: Verify test actually tests the intended behavior." 🤖 Prompt for AI Agents |
||
| def test_no_config_fails(self, tmp_path): | ||
| """Test fail when no enforcement config exists.""" | ||
| repo = _make_repo(tmp_path) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle unreadable
.husky/safely instead of crashing the assessorhusky_dir.iterdir()can raiseOSError/PermissionError; right now that bubbles out ofassess()and breaks assessment execution.As per coding guidelines, "Check for proper error handling (return skipped/error Finding, don't crash)."
Suggested fix
🤖 Prompt for AI Agents