Given / When / Then
Given a read-only gh api repos/owner/repo/... GET is invoked from within a Claude Code session with no active .claude/session/active-issue-skill marker (e.g. an agent reading file contents via gh api repos/owner/repo/contents/README.md).
When require-skill-for-issue-create.sh evaluates the command against the configured ticket.create_command_patterns, which includes the substring prefix gh api repos/.
Then the prefix match catches the GET as if it were a ticket-create attempt and blocks with BLOCKED: Raw ticket-create CLI detected (matched pattern: "gh api repos/") (exit 2).
Expected read-only gh api calls — GETs of any path, plus any call that does not target /issues with a write method — are not blocked. Only true issue-creating gh api repos/.../issues POSTs are gated.
Repro
- Ensure no
.claude/session/active-issue-skill marker is set.
- Run:
gh api repos/me2resh/apexyard/contents/README.md
- The hook blocks the call (exit 2). Expected: exit 0 — a read-only API call is not a ticket-create.
Investigation Notes — fix shape
Root cause: ticket.create_command_patterns lists gh api repos/ as a substring prefix in .claude/project-config.defaults.json. The hook matches at command boundaries — any command starting with gh api repos/... is caught, regardless of HTTP method or endpoint. The pattern was intended to catch gh api repos/owner/repo/issues -X POST -f title=... (the issue-create-via-API bypass shape), but a single substring cannot express "POST to /issues".
Fix: in require-skill-for-issue-create.sh, after the MATCHED determination, add a gh api-specific refinement that downgrades the match to a no-op unless the command BOTH targets /issues AND is a write:
case "$MATCHED" in
"gh api"*)
is_issues_endpoint=0
case "$NORM_CMD" in *"/issues"*) is_issues_endpoint=1 ;; esac
is_write=0
case "$NORM_CMD" in
*" -X POST"*|*" -XPOST"*|*" --method POST"*|*" --method=POST"*|\
*" -f "*|*" -F "*|*" --field "*|*" --raw-field "*|*" --input "*)
is_write=1 ;;
esac
if [ "$is_issues_endpoint" -ne 1 ] || [ "$is_write" -ne 1 ]; then
exit 0 # not a ticket-create — a read, or a non-issues write
fi
;;
esac
gh api defaults to GET; it becomes a write only with an explicit -X / --method, or a field flag (-f, -F, --field, --raw-field, --input — fields imply POST). Requiring both /issues AND a write signal preserves the original protection (the /issues POST shape stays blocked) while no longer blocking innocent reads.
Test coverage worth carrying
gh api repos/o/r/contents/... GET → no-op
gh api repos/o/r/issues/<id> GET → no-op (reading an issue is not creating one)
gh api repos/o/r/pulls -X POST -f ... → no-op (PR creation, not ticket-create — this hook is require-skill-for-**issue**-create)
gh api repos/o/r/issues --method POST -f title=x → still blocked
gh api repos/o/r/issues -f title=x → still blocked (field flag implies POST)
Severity
P2 — a workaround exists (APEXYARD_ALLOW_RAW_TICKET_CREATE=1 env-var escape hatch), but the false-block adds friction every time an agent reads via the API.
Given / When / Then
Given a read-only
gh api repos/owner/repo/...GET is invoked from within a Claude Code session with no active.claude/session/active-issue-skillmarker (e.g. an agent reading file contents viagh api repos/owner/repo/contents/README.md).When
require-skill-for-issue-create.shevaluates the command against the configuredticket.create_command_patterns, which includes the substring prefixgh api repos/.Then the prefix match catches the GET as if it were a ticket-create attempt and blocks with
BLOCKED: Raw ticket-create CLI detected (matched pattern: "gh api repos/")(exit 2).Expected read-only
gh apicalls — GETs of any path, plus any call that does not target/issueswith a write method — are not blocked. Only true issue-creatinggh api repos/.../issuesPOSTs are gated.Repro
.claude/session/active-issue-skillmarker is set.gh api repos/me2resh/apexyard/contents/README.mdInvestigation Notes — fix shape
Root cause:
ticket.create_command_patternslistsgh api repos/as a substring prefix in.claude/project-config.defaults.json. The hook matches at command boundaries — any command starting withgh api repos/...is caught, regardless of HTTP method or endpoint. The pattern was intended to catchgh api repos/owner/repo/issues -X POST -f title=...(the issue-create-via-API bypass shape), but a single substring cannot express "POST to /issues".Fix: in
require-skill-for-issue-create.sh, after theMATCHEDdetermination, add agh api-specific refinement that downgrades the match to a no-op unless the command BOTH targets/issuesAND is a write:gh apidefaults to GET; it becomes a write only with an explicit-X/--method, or a field flag (-f,-F,--field,--raw-field,--input— fields imply POST). Requiring both/issuesAND a write signal preserves the original protection (the/issuesPOST shape stays blocked) while no longer blocking innocent reads.Test coverage worth carrying
gh api repos/o/r/contents/...GET → no-opgh api repos/o/r/issues/<id>GET → no-op (reading an issue is not creating one)gh api repos/o/r/pulls -X POST -f ...→ no-op (PR creation, not ticket-create — this hook isrequire-skill-for-**issue**-create)gh api repos/o/r/issues --method POST -f title=x→ still blockedgh api repos/o/r/issues -f title=x→ still blocked (field flag implies POST)Severity
P2 — a workaround exists (
APEXYARD_ALLOW_RAW_TICKET_CREATE=1env-var escape hatch), but the false-block adds friction every time an agent reads via the API.