feat(nhi,ai-agents): Add support for Cursor and Claude secrets hooks#1183
feat(nhi,ai-agents): Add support for Cursor and Claude secrets hooks#1183paulpetit-gg-ext merged 1 commit intomainfrom
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1183 +/- ##
==========================================
+ Coverage 92.62% 92.76% +0.13%
==========================================
Files 160 168 +8
Lines 7756 8109 +353
==========================================
+ Hits 7184 7522 +338
- Misses 572 587 +15
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
amascia-gg
left a comment
There was a problem hiding this comment.
Code Review Summary
Overall: Well-structured feature with clean architecture (Flavor strategy pattern, good cross-tool quirk handling). A few issues to address before merge.
Critical
1. break skips remaining template keys in _fill_dict — ggshield/verticals/secret/ai_hook/installation.py:132
The break exits the for key, value in template.items() loop when a list match is found. For the Claude Code template (which has PreToolUse, PostToolUse, UserPromptSubmit at the same level), only the first hook type gets updated during reinstallation — the others are silently skipped.
_fill_dict(existing_value, value[0], command, overwrite, stats, locator)
- break
2. File handle resource leak — ggshield/verticals/secret/ai_hook/scanner.py:95
content = open(identifier, "r").read() # never closedShould use a context manager. Combined fix with items below:
3. Arbitrary file read without path validation — scanner.py:91-95
file_path from untrusted JSON is used directly to read any file on disk. Content is then sent to the GitGuardian API. Should validate path is within workspace_roots/cwd and use is_file() instead of exists().
Suggested consolidated fix for findings 2, 3, plus missing encoding and error handling:
elif tool == Tool.READ:
identifier = lookup(tool_input, ["file_path", "filePath"], "")
content = ""
try:
file_path = Path(identifier)
if file_path.is_file() and file_path.stat().st_size <= MAX_FILE_READ_SIZE:
with open(identifier, "r", encoding="utf-8") as f:
content = f.read()
except (OSError, UnicodeDecodeError):
content = ""High
4. No exception handling around notify-py — scanner.py:238-246
Will crash the hook on headless/CI environments (no display server, DBus errors). Since this runs in the POST_TOOL_USE path, the scan result would be lost and the AI tool might hang waiting for JSON output.
try:
notification = Notify()
# ...
notification.send()
except Exception:
pass # Best-effort notification5. No size limit on stdin read — cmd/secret/scan/ai_hook.py:43
stdin_content = sys.stdin.read() # unboundedConsider sys.stdin.read(MAX_STDIN_SIZE) (e.g. 10 MB).
6. Notification icon uses relative source repo path — scanner.py:245
notification.icon = "scripts/chocolatey/icon.png"This path won't exist when installed via pip/pipx. Use a package resource or remove the icon setting.
7. Inconsistent case handling in flavor detection — scanner.py:121-123
.lower() is applied for Copilot detection but not for Claude:
elif "github.copilot-chat" in data.get("transcript_path", "").lower():
flavor = Copilot()
elif "claude" in data.get("transcript_path", ""): # <-- not lowered
flavor = Claude()Medium
8. --append flag silently ignored for AI hooks — cmd/install.py
When hook_type in AI_FLAVORS, append is never used. Consider raising a UsageError if --append is passed with an AI hook type.
9. Stale docstrings — scanner.py:182,229
_message_from_secretsdocstring references parammessage(should bepayload)_send_secret_notificationdocstring referencessecrets/source(should benbr_secrets/tool/agent_name)
10. tool_response type handling is fragile — scanner.py:104
content = data.get("tool_output", "") or data.get("tool_response", {})
if isinstance(content, dict):
content = json.dumps(content)If tool_response is a list, it passes through as-is (not a dict). Consider:
raw_output = data.get("tool_output") or data.get("tool_response") or ""
content = json.dumps(raw_output) if isinstance(raw_output, (dict, list)) else str(raw_output)Test Coverage
Estimated effective branch coverage: ~55-60%. Key gaps:
scan()public method is untested — the POST_TOOL_USE notification path is completely uncovered_send_secret_notification()untestedinstall_hooks()public function untested (only_fill_dicthelper is tested)- No test uses the actual Claude Code template — would have caught the
breakbug (#1) - Missing edge cases:
EventType.OTHER, fallbackFlavor(),Tool.OTHER, directory paths, binary files
|
Here is a more detailed description of what this PR does: Add secret scanning for AI coding tool hooksSummary
DetailsNew command: Reads a hook event from stdin as JSON, scans the relevant content for secrets using the existing Supported hook event types:
Turns out this three hooks are both universal among coding assistants (naming aside) and are called in all cases that interest us. Other hooks like Cursor's Hook installation
Architecture
New dependencies
|
|
To comment on the automated review:
To summarizes, I would like other's opinion on these points:
|
xblanchot-gg
left a comment
There was a problem hiding this comment.
A few comments, but nothing really problematic. I still need to check the tests
agateau-gg
left a comment
There was a problem hiding this comment.
Looks mostly good, but I wouldn't mind if next PRs are less than 2k lines.
| if result.block: | ||
| click.echo(result.message, err=True) | ||
| return 2 | ||
| else: | ||
| click.echo("No secrets found. Good to go.") | ||
| return 0 |
There was a problem hiding this comment.
Since every class inheriting from Flavor override this method, I think it should be abstract.
There was a problem hiding this comment.
Currently if we are not able to identify which code assistant triggered the hook, we fallback to a base instance with this default implementation (returning 2 is a "universal" convention among coding tools, but prevents from having a deeper integration, like surfacing the error to the user, etc.)
So we can't make the class abstract with the current logic.
72d425d to
f722e44
Compare
|
Thanks for all your reviews! I applied your comments and rebased to main. I took the liberty to close suggestions I implemented right away. I left open those that may still need some feedback. I added a commit with your fixes, but once this PR is approved I think i will squash them because the two fix commits don't make a lot of sense by themselves. |
f722e44 to
7a4d69e
Compare
xblanchot-gg
left a comment
There was a problem hiding this comment.
Looks good to me thanks a lot Paul ! I cannot approve as I created the PR for you but you have my validation ;)
7a4d69e to
814b308
Compare
paulpetit-gg-ext
left a comment
There was a problem hiding this comment.
(approved in the name of xavier)
814b308 to
116a88a
Compare
116a88a to
946a8c3
Compare
946a8c3 to
331ab74
Compare
This PR adds support for Cursor and Claude secret detection hooks.
It's from Paul Petit, waiting for his account to be re-activated, the PR description will be detailed today.