diff --git a/config/shared/hooks/block-gh-settings.sh b/config/shared/hooks/block-gh-settings.sh index edf97ce66..0c1d62c87 100755 --- a/config/shared/hooks/block-gh-settings.sh +++ b/config/shared/hooks/block-gh-settings.sh @@ -94,7 +94,7 @@ if is_control_plane_target "$command"; then 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 + printf '%s\n' "$command" | grep -Eq '(^|[[:space:]])[^-[:space:]=?:/][^[:space:]=?/]*(:=|=)[^[:space:]]+'; then http_method=POST fi @@ -104,6 +104,7 @@ if is_control_plane_target "$command"; 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." fi diff --git a/config/shared/hooks/block-git-push.sh b/config/shared/hooks/block-git-push.sh index c21f78284..e3eb5a5eb 100755 --- a/config/shared/hooks/block-git-push.sh +++ b/config/shared/hooks/block-git-push.sh @@ -61,12 +61,21 @@ block_push() { check_destination() { local destination="$1" - destination=${destination#refs/remotes/} - if [[ $destination == */* ]]; then - local possible_branch=${destination#*/} - if is_protected_branch "$possible_branch"; then - block_push "$possible_branch" - fi + local possible_branch="" + + if [[ $destination == refs/remotes/*/* ]]; then + possible_branch=${destination#refs/remotes/} + possible_branch=${possible_branch#*/} + elif [[ $destination == refs/heads/* ]]; then + possible_branch=${destination#refs/heads/} + elif [[ $destination == heads/* ]]; then + possible_branch=${destination#heads/} + elif [[ $destination == */* ]] && git remote 2>/dev/null | grep -Fxq "${destination%%/*}"; then + possible_branch=${destination#*/} + fi + + if [[ -n $possible_branch ]] && is_protected_branch "$possible_branch"; then + block_push "$possible_branch" fi if is_protected_branch "$destination"; then block_push "$destination" @@ -322,7 +331,7 @@ inspect_command() { while IFS= read -r segment; do [[ -z $segment ]] && continue inspect_segment "$segment" "$depth" - done < <(printf '%s\n' "$candidate" | sed -E 's/[;&|]+/\n/g') + done < <(printf '%s\n' "$candidate" | tr ';&|' '\n') } inspect_command "$command" 0 diff --git a/spec/agent_github_hook_wiring_spec.sh b/spec/agent_github_hook_wiring_spec.sh index c25ea5bf4..8eae56d86 100644 --- a/spec/agent_github_hook_wiring_spec.sh +++ b/spec/agent_github_hook_wiring_spec.sh @@ -2,6 +2,24 @@ Describe 'shared GitHub guardrail wiring' +registered_hook_commands() { + local config="$1" + case "$config" in + config/codex/hooks.json | config/claude/settings.json) + jq -r '.hooks.PreToolUse[] | select(.matcher == "Bash") | .hooks[]?.command' "$config" + ;; + config/cursor/hooks.json) + jq -r '.hooks.beforeShellExecution[]?.command' "$config" + ;; + config/copilot/config.json) + jq -r '.hooks.preToolUse[]?.command' "$config" + ;; + config/grok/plugin/hooks/hooks.json) + jq -r '.hooks.PreToolUse[] | select(.matcher | test("(^|\\|)Bash($|\\|)")) | .hooks[]?.command' "$config" + ;; + esac +} + verify_wiring() { local config hook for config in \ @@ -11,7 +29,7 @@ verify_wiring() { config/copilot/config.json \ config/grok/plugin/hooks/hooks.json; do for hook in block-git-push.sh block-gh-settings.sh; do - if ! grep -Fq "config/shared/hooks/$hook" "$config"; then + if ! registered_hook_commands "$config" | grep -Fqx "\$HOME/dotfiles/config/shared/hooks/$hook"; then printf 'missing %s in %s\n' "$hook" "$config" >&2 return 1 fi diff --git a/spec/block_gh_settings_spec.sh b/spec/block_gh_settings_spec.sh index e83bc6738..81e062164 100644 --- a/spec/block_gh_settings_spec.sh +++ b/spec/block_gh_settings_spec.sh @@ -54,6 +54,18 @@ When run bash "$SCRIPT" The status should be success End +It 'allows an HTTPie GET with a query string' +Data '{"tool_input": {"command": "http https://api.github.com/repos/owner/repo/rulesets?per_page=10"}}' +When run bash "$SCRIPT" +The status should be success +End + +It 'allows an HTTPie GET with an equals-form option' +Data '{"tool_input": {"command": "http --auth-type=bearer https://api.github.com/repos/owner/repo/rulesets"}}' +When run bash "$SCRIPT" +The status should be success +End + End Describe 'blocked gh repo subcommands' @@ -233,6 +245,20 @@ The status should eq 2 The stderr should include 'BLOCKED' End +It 'blocks lowercase HTTPie mutations' +Data '{"tool_input": {"command": "http delete https://api.github.com/repos/owner/repo/hooks/1"}}' +When run bash "$SCRIPT" +The status should eq 2 +The stderr should include 'BLOCKED' +End + +It 'blocks lowercase curl request methods' +Data '{"tool_input": {"command": "curl --request delete https://api.github.com/repos/owner/repo/hooks/1"}}' +When run bash "$SCRIPT" +The status should eq 2 +The stderr should include 'BLOCKED' +End + It 'blocks xh mutations' Data '{"tool_input": {"command": "xh PUT https://api.github.com/repos/owner/repo/actions/permissions"}}' When run bash "$SCRIPT" diff --git a/spec/block_git_push_spec.sh b/spec/block_git_push_spec.sh index 7b630f82f..9aa873c5b 100644 --- a/spec/block_git_push_spec.sh +++ b/spec/block_git_push_spec.sh @@ -6,7 +6,7 @@ SCRIPT="$PWD/config/shared/hooks/block-git-push.sh" setup() { TEMP_REPO=$(mktemp -d) - git -C "$TEMP_REPO" init -q + git -C "$TEMP_REPO" init -q -b main git -C "$TEMP_REPO" config commit.gpgSign false git -C "$TEMP_REPO" config user.email agent@example.com git -C "$TEMP_REPO" config user.name Agent @@ -16,7 +16,7 @@ setup() { setup_allowed() { TEMP_REPO=$(mktemp -d) - git -C "$TEMP_REPO" init -q + git -C "$TEMP_REPO" init -q -b main git -C "$TEMP_REPO" config commit.gpgSign false git -C "$TEMP_REPO" config user.email agent@example.com git -C "$TEMP_REPO" config user.name Agent @@ -51,6 +51,18 @@ When run bash -c "cd '$TEMP_REPO' && bash '$SCRIPT'" The status should be success End +It 'allows a feature branch whose final path component is main' +Data '{"tool_input": {"command": "git push origin feature/main"}}' +When run bash -c "cd '$TEMP_REPO' && bash '$SCRIPT'" +The status should be success +End + +It 'allows a feature branch whose final path component is master' +Data '{"tool_input": {"command": "git push origin release/master"}}' +When run bash -c "cd '$TEMP_REPO' && bash '$SCRIPT'" +The status should be success +End + It 'allows pushing main to a feature destination' Data '{"tool_input": {"command": "git push origin main:feat/snapshot"}}' When run bash -c "cd '$TEMP_REPO' && bash '$SCRIPT'"