diff --git a/.claude/commands/dedupe.md b/.claude/commands/dedupe.md index 6641b5d88..edab65809 100644 --- a/.claude/commands/dedupe.md +++ b/.claude/commands/dedupe.md @@ -1,5 +1,5 @@ --- -allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(./scripts/comment-on-duplicates.sh:*) +allowed-tools: Bash(./scripts/gh.sh:*), Bash(./scripts/comment-on-duplicates.sh:*) description: Find duplicate GitHub issues --- @@ -18,6 +18,10 @@ To do this, follow these steps precisely: Notes (be sure to tell this to your agents, too): -- Use `gh` to interact with Github, rather than web fetch -- Do not use other tools, beyond `gh` and the comment script (eg. don't use other MCP servers, file edit, etc.) +- Use `./scripts/gh.sh` to interact with Github, rather than web fetch or raw `gh`. Examples: + - `./scripts/gh.sh issue view 123` — view an issue + - `./scripts/gh.sh issue view 123 --comments` — view with comments + - `./scripts/gh.sh issue list --state open --limit 20` — list issues + - `./scripts/gh.sh search issues "query" --limit 10` — search for issues +- Do not use other tools, beyond `./scripts/gh.sh` and the comment script (eg. don't use other MCP servers, file edit, etc.) - Make a todo list first diff --git a/.claude/commands/triage-issue.md b/.claude/commands/triage-issue.md index f4d0ebaac..d257b1ce9 100644 --- a/.claude/commands/triage-issue.md +++ b/.claude/commands/triage-issue.md @@ -1,5 +1,5 @@ --- -allowed-tools: Bash(gh label list:*),Bash(gh issue view:*),Bash(./scripts/edit-issue-labels.sh:*),Bash(gh search issues:*) +allowed-tools: Bash(./scripts/gh.sh:*),Bash(./scripts/edit-issue-labels.sh:*) description: Triage GitHub issues by analyzing and applying labels --- @@ -12,17 +12,21 @@ Context: $ARGUMENTS TOOLS: -- `gh label list`: Fetch all available labels in this repo -- `gh issue view NUMBER`: Read the issue title, body, and labels -- `gh issue view NUMBER --comments`: Read the conversation -- `gh search issues QUERY`: Find similar or duplicate issues -- `./scripts/edit-issue-labels.sh --issue NUMBER --add-label LABEL --remove-label LABEL`: Add or remove labels +- `./scripts/gh.sh` — wrapper for `gh` CLI. Only supports these subcommands and flags: + - `./scripts/gh.sh label list` — fetch all available labels + - `./scripts/gh.sh label list --limit 100` — fetch with limit + - `./scripts/gh.sh issue view 123` — read issue title, body, and labels + - `./scripts/gh.sh issue view 123 --comments` — read the conversation + - `./scripts/gh.sh issue list --state open --limit 20` — list issues + - `./scripts/gh.sh search issues "query"` — find similar or duplicate issues + - `./scripts/gh.sh search issues "query" --limit 10` — search with limit +- `./scripts/edit-issue-labels.sh --issue NUMBER --add-label LABEL --remove-label LABEL` — add or remove labels TASK: -1. Run `gh label list` to fetch the available labels. You may ONLY use labels from this list. Never invent new labels. -2. Run `gh issue view ISSUE_NUMBER` to read the issue details. -3. Run `gh issue view ISSUE_NUMBER --comments` to read the conversation. +1. Run `./scripts/gh.sh label list` to fetch the available labels. You may ONLY use labels from this list. Never invent new labels. +2. Run `./scripts/gh.sh issue view ISSUE_NUMBER` to read the issue details. +3. Run `./scripts/gh.sh issue view ISSUE_NUMBER --comments` to read the conversation. **If EVENT is "issues" (new issue):** @@ -31,7 +35,7 @@ TASK: 5. Analyze and apply category labels: - Type (bug, enhancement, question, etc.) - Technical areas and platform - - Check for duplicates with `gh search issues`. Only mark as duplicate of OPEN issues. + - Check for duplicates with `./scripts/gh.sh search issues`. Only mark as duplicate of OPEN issues. 6. Evaluate lifecycle labels: - `needs-repro` (bugs only, 7 days): Bug reports without clear steps to reproduce. A good repro has specific, followable steps that someone else could use to see the same issue. @@ -58,7 +62,7 @@ TASK: - Do NOT add or remove category labels (bug, enhancement, etc.) on comment events. GUIDELINES: -- ONLY use labels from `gh label list` — never create or guess label names +- ONLY use labels from `./scripts/gh.sh label list` — never create or guess label names - DO NOT post any comments to the issue - Be conservative with lifecycle labels — only apply when clearly warranted - Only apply lifecycle labels (`needs-repro`, `needs-info`) to bugs — never to questions or enhancements diff --git a/.github/workflows/claude-issue-triage.yml b/.github/workflows/claude-issue-triage.yml index 58fd22b19..21bb60730 100644 --- a/.github/workflows/claude-issue-triage.yml +++ b/.github/workflows/claude-issue-triage.yml @@ -29,6 +29,7 @@ jobs: uses: anthropics/claude-code-action@v1 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} with: github_token: ${{ secrets.GITHUB_TOKEN }} allowed_non_write_users: "*" diff --git a/scripts/gh.sh b/scripts/gh.sh new file mode 100755 index 000000000..6113a75be --- /dev/null +++ b/scripts/gh.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Wrapper around gh CLI that only allows specific subcommands and flags. +# All commands are scoped to the current repository via GH_REPO or GITHUB_REPOSITORY. +# +# Usage: +# ./scripts/gh.sh issue view 123 +# ./scripts/gh.sh issue view 123 --comments +# ./scripts/gh.sh issue list --state open --limit 20 +# ./scripts/gh.sh search issues "search query" --limit 10 +# ./scripts/gh.sh label list --limit 100 + +ALLOWED_FLAGS=(--comments --state --limit --label) +FLAGS_WITH_VALUES=(--state --limit --label) + +SUB1="${1:-}" +SUB2="${2:-}" +CMD="$SUB1 $SUB2" +case "$CMD" in + "issue view"|"issue list"|"search issues"|"label list") + ;; + *) + exit 1 + ;; +esac + +shift 2 + +# Separate flags from positional arguments +POSITIONAL=() +FLAGS=() +skip_next=false +for arg in "$@"; do + if [[ "$skip_next" == true ]]; then + FLAGS+=("$arg") + skip_next=false + elif [[ "$arg" == -* ]]; then + flag="${arg%%=*}" + matched=false + for allowed in "${ALLOWED_FLAGS[@]}"; do + if [[ "$flag" == "$allowed" ]]; then + matched=true + break + fi + done + if [[ "$matched" == false ]]; then + exit 1 + fi + FLAGS+=("$arg") + # If flag expects a value and isn't using = syntax, skip next arg + if [[ "$arg" != *=* ]]; then + for vflag in "${FLAGS_WITH_VALUES[@]}"; do + if [[ "$flag" == "$vflag" ]]; then + skip_next=true + break + fi + done + fi + else + POSITIONAL+=("$arg") + fi +done + +REPO="${GH_REPO:-${GITHUB_REPOSITORY:-}}" + +if [[ "$CMD" == "search issues" ]]; then + if [[ -z "$REPO" ]]; then + exit 1 + fi + QUERY="${POSITIONAL[0]:-}" + QUERY_LOWER=$(echo "$QUERY" | tr '[:upper:]' '[:lower:]') + if [[ "$QUERY_LOWER" == *"repo:"* || "$QUERY_LOWER" == *"org:"* || "$QUERY_LOWER" == *"user:"* ]]; then + exit 1 + fi + gh "$SUB1" "$SUB2" "$QUERY" --repo "$REPO" "${FLAGS[@]}" +else + # Reject URLs in positional args to prevent cross-repo access + for pos in "${POSITIONAL[@]}"; do + if [[ "$pos" == http://* || "$pos" == https://* ]]; then + exit 1 + fi + done + gh "$SUB1" "$SUB2" "${POSITIONAL[@]}" "${FLAGS[@]}" +fi