2026-02-25 14:35:57 +00:00
#!/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
2026-03-02 12:22:00 +00:00
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 "
2026-02-25 14:35:57 +00:00
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" )
; ;
*)
2026-03-02 12:22:00 +00:00
echo "Error: only 'issue view', 'issue list', 'search issues', 'label list' are allowed (e.g., ./scripts/gh.sh issue view 123)" >& 2
2026-02-25 14:35:57 +00:00
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
2026-03-02 12:22:00 +00:00
echo "Error: only --comments, --state, --limit, --label flags are allowed (e.g., ./scripts/gh.sh issue list --state open --limit 20)" >& 2
2026-02-25 14:35:57 +00:00
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
2026-03-02 12:22:00 +00:00
echo "Error: search query must not contain repo:, org:, or user: qualifiers (e.g., ./scripts/gh.sh search issues \"bug report\" --limit 10)" >& 2
2026-02-25 14:35:57 +00:00
exit 1
fi
gh " $SUB1 " " $SUB2 " " $QUERY " --repo " $REPO " " ${ FLAGS [@] } "
2026-03-02 12:22:00 +00:00
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 [@] } "
2026-02-25 14:35:57 +00:00
else
2026-03-02 12:22:00 +00:00
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 [@] } "
2026-02-25 14:35:57 +00:00
fi