Skip to content

fix(gh-wrapper): read GraphQL merge queries from files and --input - #445

Merged
twistedmelonman merged 2 commits into
mainfrom
claude/fix-gql-merge-guard-055823
Oct 20, 2026
Merged

twistedmelonman merged 2 commits into
mainfrom
claude/fix-gql-merge-guard-055823

Conversation

@twistedmelonman

Copy link
Copy Markdown
Member

Closes #444.

What changes

  • The GraphQL mergePullRequest guard used to grep the command line. A query passed as -F query=@file or --input file never showed up there, so it reached the real gh and skipped pre-merge-review.sh and the merge-lock.
  • The merge check now runs through the query reader from fix(gh-wrapper): close GH_REPO, PR URL and GraphQL draft bypasses #443. _gh_wrapper_block_graphql_pr_publish is renamed _gh_wrapper_block_graphql_pr_mutation, and its pattern gains mergePullRequest. A merge match keeps the old merge message. A query the wrapper cannot read (stdin, a missing file, \u escapes in --input) is refused, and the message now names both the merge risk and the off-org risk.
  • The call moves from _gh_wrapper_block_off_org_promotion into _gh_wrapper_block_bypass, gated on the parsed subcommand. The function makes one pass over the files.
  • Side effects of using the reader:
    • gh -R o/r api graphql ... is now checked. The old check required $1 == api.
    • A multi-line inline query is caught. The old grep was line-based.
    • A REST call whose field text mentions graphql mergePullRequest( no longer trips the merge guard. Only a GraphQL endpoint counts.
  • Second commit: the REST pulls/N/merge guard had the same $1 == api gap (gh -R o/r api repos/o/r/pulls/5/merge -X PUT passed). The commit review flagged it, and it now uses the parsed subcommand too.

-f query=@file is not a bypass. gh api --help says only -F/--field reads @file. -f/--raw-field posts the literal string, so the reader keeps treating it as inline, as #443 does. A test pins that.

Tests

Section 6 of bash/tests/test-gh-wrapper-off-org-guards.sh, run in standalone and function mode. It covers inline (plain, spaced, multi-line, after -R, the /graphql endpoint, the full-URL endpoint), -F @file, --field=@file, -F@file, --input, --input=, \u escape, unreadable stdin, -f @file as a literal, a REST field naming the mutation, and a REST merge after -R.

  • On origin/main's wrapper: 127 pass, 22 fail.
  • With this branch: 149 pass, 0 fail.
  • All 11 test-gh-wrapper-*.sh files pass locally.

Separate note: claude-config hook-block-api-merge.sh

The issue guessed that the Bash-tool hook misses -F query=@file. Tested read-only, with no gh run: the hook got the same {"tool_name":"Bash","tool_input":{"command":...}} JSON its bats tests use, and m.graphql held a mergePullRequest mutation. It is not a gap. A separate @file rule (claude-config#133) already blocks these:

command exit
gh api graphql -F query=@m.graphql 2
gh api graphql -f query=@m.graphql 2
gh api graphql --field query=@m.graphql 2
gh api graphql --field=query=@m.graphql 2
gh api graphql -Fquery=@m.graphql 2
gh api graphql --input m.graphql 2
gh api graphql -f query=query{viewer{login}} (control) 0

Not in scope

The commit review asked whether GraphQL enablePullRequestAutoMerge should also be refused, since it can merge once checks pass without the merge-lock. This PR does not change it. It is a policy call.

Claude Code Bot added 2 commits October 19, 2026 20:50
The GraphQL mergePullRequest guard grepped only the command line. A
query passed as -F query=@file or --input file never shows up there,
so the mutation reached the real gh and skipped pre-merge-review.sh
and the merge-lock.

#443 already reads GraphQL queries from inline fields, -F @file values
and --input files, and refuses a query it cannot read (stdin, a missing
file, \u escapes in an --input file). The merge check now runs through
that same reader: _gh_wrapper_block_graphql_pr_publish becomes
_gh_wrapper_block_graphql_pr_mutation and its pattern gains
mergePullRequest. A merge match keeps the old merge message. The
unreadable-query message now names both risks.

The check moves from the off-org promotion guard into
_gh_wrapper_block_bypass and keys on the subcommand rather than $1, so
gh -R o/r api graphql ... is checked too. Matching is no longer
line-based, so a multi-line inline query is caught. -f/--raw-field
still counts as inline only: gh posts its @file value as a literal
string and never reads the file.

Tests: section 6 of test-gh-wrapper-off-org-guards.sh adds 15 cases
per mode (30 in all); 20 fail on the wrapper before this fix.

Closes #444
The REST pulls/N/merge guard tested $1 == api, so a global flag in
front of the subcommand (gh -R o/r api repos/o/r/pulls/5/merge -X PUT)
skipped it and reached the real gh without pre-merge review or the
merge-lock. It now uses the parsed subcommand, as the GraphQL check in
the same function does. The commit review flagged this on the
previous commit.

One case per mode in test-gh-wrapper-off-org-guards.sh; both fail on
the wrapper before this fix.

Advances #444
@twistedmelonman
twistedmelonman merged commit 6146309 into main Oct 20, 2026
5 checks passed
@twistedmelonman
twistedmelonman deleted the claude/fix-gql-merge-guard-055823 branch October 20, 2026 05:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gh-wrapper: GraphQL mergePullRequest guard misses queries read from @file or --input

1 participant