Skip to content

[Bug] require-skill-for-issue-create.sh blocks read-only "gh api repos/..." GETs (overly-broad pattern) #382

@mohamedELamine

Description

@mohamedELamine

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

  1. Ensure no .claude/session/active-issue-skill marker is set.
  2. Run: gh api repos/me2resh/apexyard/contents/README.md
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions