Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/shared/hooks/block-gh-settings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latent: HTTPie query-param syntax still false-blocked.

The new regex correctly excludes URL/option shapes, but HTTPie's URL query-parameter form k==v still matches: the middle class stops before the first =, then (:=|=) consumes the first =, and [^[:space:]]+ greedily eats =value.

Repro on any protected suffix:

printf '{"tool_input":{"command":"http https://api.github.com/repos/o/r/hooks per_page==10"}}' \
  | bash config/shared/hooks/block-gh-settings.sh
# exit 2: A direct POST request to a repository control-plane endpoint was requested.

This is pre-existing behavior (the old regex had it too), so it's not a regression — but since this PR is loosening HTTPie handling anyway, worth teaching the regex that == is a URL query param, not a body. One option is to require the value not start with =, e.g. (:=|=)[^=[:space:]][^[:space:]]*.

http_method=POST
fi

Expand All @@ -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
Expand Down
23 changes: 16 additions & 7 deletions config/shared/hooks/block-git-push.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion spec/agent_github_hook_wiring_spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latent: exact-string matcher check will miss compound matchers.

For Codex/Claude you use select(.matcher == "Bash"), while Grok (line 18) already uses the more permissive test("(^|\\|)Bash($|\\|)"). If either Codex or Claude ever adopts a compound matcher such as "Bash|Edit" (Claude-side documentation supports this shape), the extraction returns nothing and the wiring test would fail even though the hook is correctly registered. Applying the same regex form uniformly across all three branches would be forward-compatible.

;;
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 \
Expand All @@ -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
Expand Down
26 changes: 26 additions & 0 deletions spec/block_gh_settings_spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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"
Expand Down
16 changes: 14 additions & 2 deletions spec/block_git_push_spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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'"
Expand Down
Loading