#!/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 export GH_HOST=github.com REPO="${GH_REPO:-${GITHUB_REPOSITORY:-}}" if [[ -z "$REPO" || "$REPO" == */*/* || "$REPO" != */* ]]; then echo "Error: GH_REPO or GITHUB_REPOSITORY must be set to owner/repo format (e.g., GITHUB_REPOSITORY=anthropics/claude-code)" >&2 exit 1 fi export GH_REPO="$REPO" 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") ;; *) echo "Error: only 'issue view', 'issue list', 'search issues', 'label list' are allowed (e.g., ./scripts/gh.sh issue view 123)" >&2 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 echo "Error: only --comments, --state, --limit, --label flags are allowed (e.g., ./scripts/gh.sh issue list --state open --limit 20)" >&2 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 if [[ "$CMD" == "search issues" ]]; then QUERY="${POSITIONAL[0]:-}" QUERY_LOWER=$(echo "$QUERY" | tr '[:upper:]' '[:lower:]') if [[ "$QUERY_LOWER" == *"repo:"* || "$QUERY_LOWER" == *"org:"* || "$QUERY_LOWER" == *"user:"* ]]; then echo "Error: search query must not contain repo:, org:, or user: qualifiers (e.g., ./scripts/gh.sh search issues \"bug report\" --limit 10)" >&2 exit 1 fi gh "$SUB1" "$SUB2" "$QUERY" --repo "$REPO" "${FLAGS[@]}" elif [[ "$CMD" == "issue view" ]]; then if [[ ${#POSITIONAL[@]} -ne 1 ]] || ! [[ "${POSITIONAL[0]}" =~ ^[0-9]+$ ]]; then echo "Error: issue view requires exactly one numeric issue number (e.g., ./scripts/gh.sh issue view 123)" >&2 exit 1 fi gh "$SUB1" "$SUB2" "${POSITIONAL[0]}" "${FLAGS[@]}" else if [[ ${#POSITIONAL[@]} -ne 0 ]]; then echo "Error: issue list and label list do not accept positional arguments (e.g., ./scripts/gh.sh issue list --state open, ./scripts/gh.sh label list --limit 100)" >&2 exit 1 fi gh "$SUB1" "$SUB2" "${FLAGS[@]}" fi