From a0d5240905879820adc675c2ec3d6cb00b41f3b0 Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:39:12 -0500 Subject: [PATCH 1/2] feat(git-guards): block gh pr comment, enforce GraphQL review threads gh pr comment creates top-level issue comments that cannot be resolved or tracked in PR reviews. Add DENY_GH list to git-permission-guard that blocks this command and directs to GitHub GraphQL API for proper review threads with file/line references. (claude) --- git-guards/scripts/git-permission-guard.py | 29 +++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/git-guards/scripts/git-permission-guard.py b/git-guards/scripts/git-permission-guard.py index aea1488..5b1d0d0 100755 --- a/git-guards/scripts/git-permission-guard.py +++ b/git-guards/scripts/git-permission-guard.py @@ -56,6 +56,25 @@ ("pr merge", "Merges PR - ONLY do when user EXPLICITLY requests"), ] +DENY_GH = [ + ("pr comment", ( + "BLOCKED: gh pr comment creates top-level issue comments that cannot be resolved or tracked.\n" + "\n" + "For code review comments, you MUST use GitHub GraphQL API to create proper review threads:\n" + "\n" + " 1. Create a review with inline comments (file + line references):\n" + " gh api graphql -f query='mutation { addPullRequestReview(input: { pullRequestId: \"PR_NODE_ID\", event: COMMENT, threads: [{ path: \"file.py\", line: 10, body: \"comment\" }] }) { pullRequestReview { id } } }'\n" + "\n" + " 2. Reply to an existing review thread:\n" + " gh api graphql -f query='mutation { addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: \"PRRT_xxx\", body: \"reply\" }) { comment { id } } }'\n" + "\n" + " 3. Resolve a thread after addressing it:\n" + " gh api graphql -f query='mutation { resolveReviewThread(input: { threadId: \"PRRT_xxx\" }) { thread { isResolved } } }'\n" + "\n" + "These create resolvable, line-specific review threads — the only acceptable way to post review feedback on PRs." + )), +] + def deny(reason: str) -> None: """Output deny decision and exit.""" @@ -134,13 +153,21 @@ def main(): else: subcommand = command[3:] if command.startswith("gh ") else "" + sub_tokens = subcommand.split() + + # Check DENY_GH patterns (token prefix match on gh subcommand) + if is_gh: + for pattern, reason in DENY_GH: + tokens = pattern.split() + if sub_tokens[:len(tokens)] == tokens: + deny(reason) + # Check ASK patterns - use word boundaries to avoid false matches # (e.g., "merge" shouldn't match "emergency") patterns = ASK_GIT if is_git else ASK_GH for cmd, risk in patterns: # Match as exact token sequence at start of subcommand cmd_tokens = cmd.split() - sub_tokens = subcommand.split() if len(sub_tokens) >= len(cmd_tokens) and sub_tokens[:len(cmd_tokens)] == cmd_tokens: ask(command, risk) From 70dcaee983b3ab36d598a9cbbccb6a642a864a61 Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:55:46 -0500 Subject: [PATCH 2/2] fix(git-guards): address PR review feedback on DENY_GH message and empty pattern guard - Remove redundant 'BLOCKED:' prefix from DENY_GH reason (deny() already prepends it) - Replace inline GraphQL examples with references to documented workflows to prevent staleness and flag inconsistency with repo conventions - Add `if tokens` guard in DENY_GH check to prevent empty-pattern match-all Co-Authored-By: Claude --- git-guards/scripts/git-permission-guard.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/git-guards/scripts/git-permission-guard.py b/git-guards/scripts/git-permission-guard.py index 5b1d0d0..22131a5 100755 --- a/git-guards/scripts/git-permission-guard.py +++ b/git-guards/scripts/git-permission-guard.py @@ -58,20 +58,16 @@ DENY_GH = [ ("pr comment", ( - "BLOCKED: gh pr comment creates top-level issue comments that cannot be resolved or tracked.\n" + "gh pr comment creates top-level issue comments that cannot be resolved or tracked.\n" "\n" - "For code review comments, you MUST use GitHub GraphQL API to create proper review threads:\n" + "For code review feedback, you MUST use review threads (line-specific, resolvable comments) instead.\n" "\n" - " 1. Create a review with inline comments (file + line references):\n" - " gh api graphql -f query='mutation { addPullRequestReview(input: { pullRequestId: \"PR_NODE_ID\", event: COMMENT, threads: [{ path: \"file.py\", line: 10, body: \"comment\" }] }) { pullRequestReview { id } } }'\n" + "Use the documented thread workflows for creating review comments, replying, and resolving threads:\n" + " - github-workflows/skills/resolve-pr-threads/graphql-queries.md\n" + " - github-workflows/skills/resolve-pr-threads/rest-api-patterns.md\n" "\n" - " 2. Reply to an existing review thread:\n" - " gh api graphql -f query='mutation { addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: \"PRRT_xxx\", body: \"reply\" }) { comment { id } } }'\n" - "\n" - " 3. Resolve a thread after addressing it:\n" - " gh api graphql -f query='mutation { resolveReviewThread(input: { threadId: \"PRRT_xxx\" }) { thread { isResolved } } }'\n" - "\n" - "These create resolvable, line-specific review threads — the only acceptable way to post review feedback on PRs." + "These workflows create resolvable, line-specific review threads — the only acceptable way to post review\n" + "feedback on PRs." )), ] @@ -159,7 +155,7 @@ def main(): if is_gh: for pattern, reason in DENY_GH: tokens = pattern.split() - if sub_tokens[:len(tokens)] == tokens: + if tokens and sub_tokens[:len(tokens)] == tokens: deny(reason) # Check ASK patterns - use word boundaries to avoid false matches