fix(agents): harden GitHub mutation guards - #2207
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe pull request expands shared hooks for GitHub settings and protected Git pushes. It adds command parsing for multiple clients and request forms, broad regression coverage, documentation, and wiring checks across supported agent configurations. ChangesGitHub guardrails
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant ToolClient
participant block-gh-settings.sh
participant GitHubControlPlane
ToolClient->>block-gh-settings.sh: Submit GitHub command
block-gh-settings.sh->>block-gh-settings.sh: Detect protected mutation
block-gh-settings.sh-->>ToolClient: Block with exit status 2
block-gh-settings.sh->>GitHubControlPlane: Permit allowed operation
sequenceDiagram
participant ToolClient
participant block-git-push.sh
participant GitRemote
ToolClient->>block-git-push.sh: Submit git push command
block-git-push.sh->>GitRemote: Resolve remote and default branch
block-git-push.sh->>block-git-push.sh: Evaluate destination refspec
block-git-push.sh-->>ToolClient: Block protected destination or allow push
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
spec/block_git_push_spec.sh (1)
7-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPin the initial branch name in both setup functions.
git initusesinit.defaultBranch, so latergit configcalls do not set the initial branch. Withinit.defaultBranch=trunk, the repository createstrunkinstead ofmain, and bare-push tests that expect amainbranch can fail. Usegit -C "$TEMP_REPO" init -q -b mainin bothsetupandsetup_allowed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@spec/block_git_push_spec.sh` around lines 7 - 23, Pin the initial branch to main in both setup and setup_allowed by adding the branch option to each git init invocation. Leave the remaining repository configuration and test setup unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@config/shared/hooks/block-gh-settings.sh`:
- Around line 95-99: Update the HTTPie body-detection regex in the http_method
assignment block to match only non-flag request items: exclude tokens beginning
with '-' and any token containing '?' before an '='. Preserve detection of
ordinary `name=value` and `name:=value` body arguments while preventing query
strings and option values from setting http_method=POST.
- Around line 79-108: Normalize http_method to uppercase after all
method-detection branches and before the final ^(POST|PATCH|PUT|DELETE)$
comparison in the command classification flow. Ensure captured lowercase methods
from HTTPie/https/xh and --request options are converted while preserving the
existing blocking behavior.
In `@config/shared/hooks/block-git-push.sh`:
- Around line 62-74: Update check_destination so slash-based splitting only
occurs for remote-qualified destinations or refs/heads/ and heads/ prefixes,
rather than any destination containing “/”. Preserve direct protected-branch
checks for ordinary branch names, including feature/main and release/master, and
continue blocking only when the resulting branch is protected.
- Around line 318-329: Update inspect_command to replace the sed-based command
splitting with the portable tr ';&|' newline conversion, preserving the existing
loop that skips empty segments and invokes inspect_segment for each command
segment.
In `@spec/agent_github_hook_wiring_spec.sh`:
- Around line 14-17: Update the hook validation in the wiring spec to parse each
configuration structurally and verify the command is registered under the
PreToolUse hook entry, rather than searching for the path anywhere in the JSON.
Follow the schema and command-entry contract demonstrated by
activate_config_spec.sh and config/codex/hooks.json, while preserving the
existing missing-hook failure behavior.
---
Nitpick comments:
In `@spec/block_git_push_spec.sh`:
- Around line 7-23: Pin the initial branch to main in both setup and
setup_allowed by adding the branch option to each git init invocation. Leave the
remaining repository configuration and test setup unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: cf8741f7-ad9a-4974-b108-6896aa147f9e
📒 Files selected for processing (6)
config/shared/hooks/README.mdconfig/shared/hooks/block-gh-settings.shconfig/shared/hooks/block-git-push.shspec/agent_github_hook_wiring_spec.shspec/block_gh_settings_spec.shspec/block_git_push_spec.sh
| http_method=$(explicit_method "$command") | ||
|
|
||
| if printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])(http|https|xh)[[:space:]]+(POST|PATCH|PUT|DELETE)([[:space:]]|$)'; then | ||
| http_method=$(printf '%s\n' "$command" | sed -nE 's/.*(^|[;&|[:space:]])(http|https|xh)[[:space:]]+(POST|PATCH|PUT|DELETE)([[:space:]]|$).*/\3/ip' | tail -1) | ||
| fi | ||
|
|
||
| if [[ -z $http_method ]]; then | ||
| http_method=$(printf '%s\n' "$command" | sed -nE 's/.*(^|[[:space:]])--request(=|[[:space:]]+)(POST|PATCH|PUT|DELETE)([[:space:]]|$).*/\3/ip' | tail -1) | ||
| fi | ||
|
|
||
| if [[ -z $http_method ]] && \ | ||
| printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])curl([[:space:]]|$)' && \ | ||
| printf '%s\n' "$command" | grep -Eiq '(^|[[:space:]])(--data[^[:space:]]*|-d|--form|-F|--json|--upload-file|-T)(=|[[:space:]])'; then | ||
| http_method=POST | ||
| fi | ||
|
|
||
| if [[ -z $http_method ]] && \ | ||
| printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])(http|https|xh)([[:space:]]|$)' && \ | ||
| printf '%s\n' "$command" | grep -Eq '(^|[[:space:]])[^[:space:]=]+(:=|=)[^[:space:]]+'; then | ||
| http_method=POST | ||
| fi | ||
|
|
||
| if [[ -z $http_method ]] && \ | ||
| printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])wget([[:space:]]|$)' && \ | ||
| printf '%s\n' "$command" | grep -Eiq '(^|[[:space:]])(--post-data|--post-file|--body-data)(=|[[:space:]])'; then | ||
| http_method=POST | ||
| fi | ||
|
|
||
| if [[ $http_method =~ ^(POST|PATCH|PUT|DELETE)$ ]]; then | ||
| block_settings "A direct $http_method request to a repository control-plane endpoint was requested." |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does HTTPie accept a lowercase request method such as http delete https://example.com`?`
💡 Result:
Yes, HTTPie accepts lowercase request methods such as http delete https://example.com [1][2]. While HTTPie documentation typically displays methods in uppercase (e.g., GET, POST, DELETE), the CLI tool is case-insensitive regarding the request method argument and will correctly process lowercase input [1][3].
Citations:
- 1: https://httpie.io/docs/cli/HEAD/http-method
- 2: https://httpie.io/docs/cli
- 3: https://httpie.io/docs/cli/http-method
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
git ls-files | grep -Fx 'config/shared/hooks/block-gh-settings.sh' || true
echo "== file outline/stat =="
wc -l config/shared/hooks/block-gh-settings.sh
ast-grep outline config/shared/hooks/block-gh-settings.sh || true
echo "== relevant lines =="
sed -n '1,130p' config/shared/hooks/block-gh-settings.sh
echo "== deterministic guard behavior for lowercase method assignments =="
python3 - <<'PY'
import re
def explicit_method(command, verbose=False):
method = None
if verbose and 'http' in command or 'httpie' in command:
pass
return method
def current_block_analysis(command):
# mirrors the assignment paths enough to show stored value before final regex
match = re.search(r'(^|[;&|[:space:]])http([[:space:]]+)', command, re.I)
if match:
rest = re.split(r'[\s;|&]+', command[match.end():], maxsplit=1)[0] if command[match.end():] else ''
http_method = rest.upper()
else:
http_method = None
if re.search(r'(^|[;&|[:space:]])https([[:space:]]+)', command, re.I):
rest = re.split(r'[\s;|&]+', command[match.end():] if match else command, maxsplit=1)[0] if ((match and command[match.end():]) or (not match)) else ''
http_method = rest.upper()
else:
pass
# lines 82/86 from source if explicit_method returned None and command matches.
m = re.search(r'.*(^|[;&|[:space:]])(http|https|xh)[[:space:]]+([A-Za-z]+)([[:space:]]|$).*', command, re.I)
if not http_method and m:
http_method = m.group(3)
# line 86 if still empty via curl --request; omit because current examples use implicit lowercase.
if re.search(r'(^|[;&|[:space:]])curl([[:space:]]|$)', command, re.I) and re.search(r'(^|[[:space:]])(--data[^[:space:]]*|-d|--form|-F|--json|--upload-file|-T)(=|[[:space:]])', command, re.I):
if not http_method:
http_method = 'POST'
if re.search(r'(^|[;&|[:space:]])(http|https|xh)([[:space:]]|$)', command, re.I) and re.search(r'(^|[[:space:]])[^[:space:]=]+(:=|=)[^[:space:]]+', command):
if not http_method:
http_method = 'POST'
if re.search(r'(^|[;&|[:space:]])wget([[:space:]]|$)', command, re.I) and re.search(r'(^|[[:space:]])(--post-data|--post-file|--body-data)(=|[[:space:]])', command, re.I):
if not http_method:
http_method = 'POST'
blocked = False
if http_method and re.search(r'^(POST|PATCH|PUT|DELETE)$', http_method):
blocked = True
return http_method, blocked
for cmd in [
"http delete https://api.github.com/repos/owner/repo/hooks/1",
"http DELETE https://api.github.com/repos/owner/repo/hooks/1",
"curl --request delete https://api.github.com/repos/owner/repo",
"curl --request delete -d x https://api.github.com/repos/owner/repo",
]:
print(cmd, current_block_analysis(cmd))
PYRepository: shunkakinoki/dotfiles
Length of output: 6484
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== deterministic extraction from source =="
python3 - <<'PY'
import re
source = open('config/shared/hooks/block-gh-settings.sh').read()
def explicit_method(command):
method = None
for pattern in (
r'.*(^|[[:space:]])(-X|--method)(=|[[:space:]]+)(GET|POST|PATCH|PUT|DELETE)([[:space:]]|$).*',
r'.*(^|[[:space:]])-X(GET|POST|PATCH|PUT|DELETE)([[:space:]]|$).*',
):
m = re.search(pattern, command, re.I)
if m:
method = m.group(4) or m.group(2)
return (method or '').upper()
def command_stores_raw_lowercase(command):
# Returns True if the HTTPie/https/xh capture path at lines 82/86 runs
# and captures a lowercase method before the final guard.
if not re.search(r'(^|[;&|[:space:]])http_method=\(explicit_method "$command"\)', command, re.I):
pass
m = re.search(r'.*(^|[;&|[:space:]])(http|https|xh)[[:space:]]+([^[:space:]]+)([[:space:]]|$).*', command, re.I)
if m:
return (m.group(3) != m.group(3).upper())
return False
def final_block_stops_http_method(command):
http_method = explicit_method(command)
# The guard only runs for lines 82-86 because the script assigns
# explicit_method first and then the sed capture only when command contains http|https|xh.
if re.search(r'(^|[;&|[:space:]])http_method=\(explicit_method "$command"\)', source, re.I):
pass
if re.search(r'(^|[;&|[:space:]])(http|https|xh)[[:space:]]+([A-Za-z]+)([[:space:]]|$)', command, re.I):
m = re.search(r'.*(^|[;&|[:space:]])(http|https|xh)[[:space:]]+([A-Za-z]+)([[:space:]]|$).*', command, re.I)
if m:
http_method = m.group(3)
if re.search(r'^(POST|PATCH|PUT|DELETE)$', http_method):
return True
return False
for cmd in [
"http delete https://api.github.com/repos/owner/repo/hooks/1",
"http DELETE https://api.github.com/repos/owner/repo/hooks/1",
"curl --request delete https://api.github.com/repos/owner/repo",
"curl --request delete -d x https://api.github.com/repos/owner/repo",
]:
print("cmd:", cmd)
print("explicit_method:", explicit_method(cmd))
print("sed captures lowercase:", command_stores_raw_lowercase(cmd))
print("final block matches current source:", final_block_stops_http_method(cmd))
PYRepository: shunkakinoki/dotfiles
Length of output: 1326
Uppercase the HTTP method before the final comparison.
The HTTPie/https/xh capture path assigns the raw sed capture, so lowercase methods like http delete ..., curl --request delete ..., or --request delete are not matched by the final ^(POST|PATCH|PUT|DELETE)$ guard. Normalize the method once before that comparison.
🛡️ Proposed fix
- if [[ $http_method =~ ^(POST|PATCH|PUT|DELETE)$ ]]; then
+ http_method=${http_method^^}
+ if [[ $http_method =~ ^(POST|PATCH|PUT|DELETE)$ ]]; then📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| http_method=$(explicit_method "$command") | |
| if printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])(http|https|xh)[[:space:]]+(POST|PATCH|PUT|DELETE)([[:space:]]|$)'; then | |
| http_method=$(printf '%s\n' "$command" | sed -nE 's/.*(^|[;&|[:space:]])(http|https|xh)[[:space:]]+(POST|PATCH|PUT|DELETE)([[:space:]]|$).*/\3/ip' | tail -1) | |
| fi | |
| if [[ -z $http_method ]]; then | |
| http_method=$(printf '%s\n' "$command" | sed -nE 's/.*(^|[[:space:]])--request(=|[[:space:]]+)(POST|PATCH|PUT|DELETE)([[:space:]]|$).*/\3/ip' | tail -1) | |
| fi | |
| if [[ -z $http_method ]] && \ | |
| printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])curl([[:space:]]|$)' && \ | |
| printf '%s\n' "$command" | grep -Eiq '(^|[[:space:]])(--data[^[:space:]]*|-d|--form|-F|--json|--upload-file|-T)(=|[[:space:]])'; then | |
| http_method=POST | |
| fi | |
| if [[ -z $http_method ]] && \ | |
| printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])(http|https|xh)([[:space:]]|$)' && \ | |
| printf '%s\n' "$command" | grep -Eq '(^|[[:space:]])[^[:space:]=]+(:=|=)[^[:space:]]+'; then | |
| http_method=POST | |
| fi | |
| if [[ -z $http_method ]] && \ | |
| printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])wget([[:space:]]|$)' && \ | |
| printf '%s\n' "$command" | grep -Eiq '(^|[[:space:]])(--post-data|--post-file|--body-data)(=|[[:space:]])'; then | |
| http_method=POST | |
| fi | |
| if [[ $http_method =~ ^(POST|PATCH|PUT|DELETE)$ ]]; then | |
| block_settings "A direct $http_method request to a repository control-plane endpoint was requested." | |
| http_method=$(explicit_method "$command") | |
| if printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])(http|https|xh)[[:space:]]+(POST|PATCH|PUT|DELETE)([[:space:]]|$)'; then | |
| http_method=$(printf '%s\n' "$command" | sed -nE 's/.*(^|[;&|[:space:]])(http|https|xh)[[:space:]]+(POST|PATCH|PUT|DELETE)([[:space:]]|$).*/\3/ip' | tail -1) | |
| fi | |
| if [[ -z $http_method ]]; then | |
| http_method=$(printf '%s\n' "$command" | sed -nE 's/.*(^|[[:space:]])--request(=|[[:space:]]+)(POST|PATCH|PUT|DELETE)([[:space:]]|$).*/\3/ip' | tail -1) | |
| fi | |
| if [[ -z $http_method ]] && \ | |
| printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])curl([[:space:]]|$)' && \ | |
| printf '%s\n' "$command" | grep -Eiq '(^|[[:space:]])(--data[^[:space:]]*|-d|--form|-F|--json|--upload-file|-T)(=|[[:space:]])'; then | |
| http_method=POST | |
| fi | |
| if [[ -z $http_method ]] && \ | |
| printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])(http|https|xh)([[:space:]]|$)' && \ | |
| printf '%s\n' "$command" | grep -Eq '(^|[[:space:]])[^[:space:]=]+(:=|=)[^[:space:]]+'; then | |
| http_method=POST | |
| fi | |
| if [[ -z $http_method ]] && \ | |
| printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])wget([[:space:]]|$)' && \ | |
| printf '%s\n' "$command" | grep -Eiq '(^|[[:space:]])(--post-data|--post-file|--body-data)(=|[[:space:]])'; then | |
| http_method=POST | |
| fi | |
| http_method=${http_method^^} | |
| if [[ $http_method =~ ^(POST|PATCH|PUT|DELETE)$ ]]; then | |
| block_settings "A direct $http_method request to a repository control-plane endpoint was requested." |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/shared/hooks/block-gh-settings.sh` around lines 79 - 108, Normalize
http_method to uppercase after all method-detection branches and before the
final ^(POST|PATCH|PUT|DELETE)$ comparison in the command classification flow.
Ensure captured lowercase methods from HTTPie/https/xh and --request options are
converted while preserving the existing blocking behavior.
| if [[ -z $http_method ]] && \ | ||
| printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])(http|https|xh)([[:space:]]|$)' && \ | ||
| printf '%s\n' "$command" | grep -Eq '(^|[[:space:]])[^[:space:]=]+(:=|=)[^[:space:]]+'; then | ||
| http_method=POST | ||
| fi |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Restrict the HTTPie body detection to non-flag request items.
The pattern [^[:space:]=]+(:=|=)[^[:space:]]+ matches any token that contains =. A query string or an option value therefore sets http_method=POST. For example, http https://api.github.com/repos/owner/repo/rulesets?per_page=10 is a read, but the hook blocks it. Options such as --auth-type=bearer cause the same result.
Exclude tokens that start with - and tokens that contain ? before the =.
♻️ Proposed fix
- printf '%s\n' "$command" | grep -Eq '(^|[[:space:]])[^[:space:]=]+(:=|=)[^[:space:]]+'; then
+ printf '%s\n' "$command" | grep -Eq '(^|[[:space:]])[^-[:space:]=?:/][^[:space:]=?/]*(:=|=)[^[:space:]]+'; then📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if [[ -z $http_method ]] && \ | |
| printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])(http|https|xh)([[:space:]]|$)' && \ | |
| printf '%s\n' "$command" | grep -Eq '(^|[[:space:]])[^[:space:]=]+(:=|=)[^[:space:]]+'; then | |
| http_method=POST | |
| fi | |
| if [[ -z $http_method ]] && \ | |
| printf '%s\n' "$command" | grep -Eiq '(^|[;&|[:space:]])(http|https|xh)([[:space:]]|$)' && \ | |
| printf '%s\n' "$command" | grep -Eq '(^|[[:space:]])[^-[:space:]=?:/][^[:space:]=?/]*(:=|=)[^[:space:]]+'; then | |
| http_method=POST | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@config/shared/hooks/block-gh-settings.sh` around lines 95 - 99, Update the
HTTPie body-detection regex in the http_method assignment block to match only
non-flag request items: exclude tokens beginning with '-' and any token
containing '?' before an '='. Preserve detection of ordinary `name=value` and
`name:=value` body arguments while preventing query strings and option values
from setting http_method=POST.
| if ! grep -Fq "config/shared/hooks/$hook" "$config"; then | ||
| printf 'missing %s in %s\n' "$hook" "$config" >&2 | ||
| return 1 | ||
| fi |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Validate the active hook registration, not only the path text.
grep -Fq searches the entire JSON file. A stale value or unrelated field can satisfy the check while the hook is not registered under PreToolUse. Parse each configuration according to its schema and assert the command entry. The existing spec/activate_config_spec.sh:160-170 and config/codex/hooks.json:55-61 show the structural contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@spec/agent_github_hook_wiring_spec.sh` around lines 14 - 17, Update the hook
validation in the wiring spec to parse each configuration structurally and
verify the command is registered under the PreToolUse hook entry, rather than
searching for the path anywhere in the JSON. Follow the schema and command-entry
contract demonstrated by activate_config_spec.sh and config/codex/hooks.json,
while preserving the existing missing-hook failure behavior.
7007f74 to
3684767
Compare
| index=$((index + 2)) | ||
| continue | ||
| ;; | ||
| -C* | --git-dir=* | --work-tree=* | --namespace=* | --no-pager | --paginate | --literal-pathspecs | --no-literal-pathspecs | --glob-pathspecs | --noglob-pathspecs | --icase-pathspecs) |
There was a problem hiding this comment.
git -c foo=bar push bypasses this guard. -c is a valued flag (like -C, --git-dir, --work-tree, --namespace — all special-cased above), but it isn't enumerated here, so it falls through to the generic -*) branch on line 277 which only skips one token. That makes the next token — the name=value config pair — the detected "subcommand", so push is never seen and analyze_push_words never runs. Reproduced:
$ echo '{"tool_input": {"command": "git -c push.default=matching push origin main"}}' \
| bash config/shared/hooks/block-git-push.sh; echo "exit: $?"
exit: 0
$ echo '{"tool_input": {"command": "git -c foo=bar push origin main"}}' \
| bash config/shared/hooks/block-git-push.sh; echo "exit: $?"
exit: 0
Fix: add -c to the first case arm (consumes 2 tokens) and -c* to the second (single-token -cKEY=VAL form).
Summary
Validation
Security boundary
These local hooks are bypassable defense-in-depth guardrails. Restricted GitHub credentials and server-side rulesets remain authoritative.