Skip to content

claude.yml default claude-args: the gh api deny patterns do not deny writes #616

Description

@d-morrison

Summary

claude.yml's default claude-args allows Bash(gh api:*) and denies three prefix patterns:

Bash(gh api -X:*),Bash(gh api --method:*),Bash(gh api graphql:*)

The inline comment states the intent:

gh api is allowed for the read-only polling loop, but its write forms (-X/--method mutations and graphql) are denied so a write-scoped GITHUB_TOKEN can't be used for arbitrary API mutations

The deny list does not implement that intent. Two independent gaps, and the agent runs with contents: write, issues: write, pull-requests: write, and actions: write.

Gap 1: a POST needs no -X

gh api --help:

The default HTTP request method is GET normally and POST if any parameters were added. Override the method with --method.

Verified against the installed gh on 2026-08-24. So every -f / -F / --input call is a POST that matches the gh api:* allow and none of the three deny patterns. The CLI's own help text uses exactly this form as its worked example -- an issue-comment POST written with -f body=... and no -X anywhere in it.

Gap 2: the patterns are prefix matches, so argument order defeats them

Bash(gh api -X:*) matches only a command beginning gh api -X. Reordering is enough: putting a -H flag first and -X POST second produces an equivalent request that matches no deny pattern, since gh accepts flags in any order.

Why it matters

The comment presents the denials as the reason a write-scoped token is safe, and the same block explains that git push is handled by omission rather than denial precisely because enumerating deny patterns is fragile:

That sidesteps the whole class of destructive push variants rather than enumerating deny patterns

gh api then does the enumerating-deny-patterns thing anyway, on a surface with more variants than git push.

The exposure is bounded by the trusted-author gate, which the same comment already names as "the primary containment" -- so this is not a break-glass hole. It is a stated defense-in-depth control that does not hold, which is worth either fixing or restating.

Options

  1. Drop Bash(gh api:*) from the allow list and enumerate the read paths the polling loop actually needs. Matches how git push is already handled -- omission over denial. Most robust, most work.
  2. Keep the allow and fix the wording, stating that gh api writes are bounded by the trusted-author gate rather than by the deny list. Cheapest, and honest.
  3. Leave the patterns as a speed bump but stop describing them as denying "write forms".

Option 1 is the real fix; option 2 is worth doing immediately either way, since the current comment would let a reader conclude the control exists.

Provenance

Found by an adversarial review of a consumer migration adopting this workflow (UCD-SERG/ucd-serg.github.io#111), while fact-checking a comment in the caller stub that paraphrased this one. The consumer-side comment has been corrected to describe what is actually denied.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions