fix(#382): refine gh api matcher to only block POST on /issues#384
Conversation
The pattern `gh api repos/` in ticket.create_command_patterns was a pure substring prefix with no HTTP-method or endpoint refinement, so any read-only `gh api repos/owner/repo/contents/...` GET was blocked as if it were a ticket-create POST. Add a case-match for the `gh api` family after $MATCHED is set: if the command does not BOTH target /issues AND carry a write signal (-X POST / --method POST / -f / -F / --field / --raw-field / --input), downgrade the match to exit 0 (no-op). Other tracker CLIs (gh issue create, linear issue create, jira issue create, asana task create) are unaffected — their patterns stay pure substring prefixes. Regression test added: .claude/hooks/tests/test_require_skill_for_issue_create_gh_api.sh covers GET on /contents, GET on /issues/<id>, POST on /pulls (not /issues), POST on /issues via --method, POST on /issues via field flag, and gh issue create. 6/6 PASS locally. Closes #382 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review: PR #384
Commit: 584143789612292cc24f7b7648323870a1b76980
Summary
Surgical refinement of the gh api repos/ matcher in require-skill-for-issue-create.sh so it only blocks POSTs against /issues endpoints. Previously the pure-substring prefix caught read-only GETs (e.g. gh api repos/o/r/contents/README.md) as if they were ticket-create attempts. Closes #382.
Checklist Results
- Architecture & Design: Pass
- Code Quality: Pass
- Testing: Pass
- Security: Pass (the gate against actual
/issuesPOSTs is preserved; the escape hatch and bootstrap exemption paths remain untouched) - Performance: Pass (one extra
casestatement on a code path that only runs after a match) - PR Description & Glossary: Pass (4 well-explained terms)
- Summary Bullet Narrative: Pass (every bullet has what + why)
- Technical Decisions (AgDR):N/A (surgical bug fix, not a new decision)
- Adopter Handbooks: N/A (no handbooks loaded for shell-script hook fixes)
Diff correctness
- Refinement block is placed exactly where it should be: AFTER
MATCHEDis determined (so we know we matched something) and BEFORE the bootstrap / active-issue-skill exemptions (so a non-/issues-writegh apicall short-circuits cleanly and doesn't waste cycles on exemption resolution). - Case statement is properly scoped:
"gh api"*)is the ONLY arm — all other matched patterns (gh issue create,linear issue create,jira issue create,asana task create) fall through unchanged. Surgical as advertised. - Write-signal enumeration is complete and matches the gh-cli reality:
-X POST/-XPOST/--method POST/--method=POSTcovers explicit method flags (both equals and space forms);-f/-F/--field/--raw-field/--inputcovers field flags that implicitly switch gh from GET to POST. NORM_CMDis whitespace-collapsed viatr -s '[:space:]' ' 'BEFORE the case match, so the pattern's required leading space (*" -f "*etc.) is reliable.- Block-list narrowing (
is_issues_endpoint=1ANDis_write=1both required) preserves the original protection against the/issuesPOST shape while no longer false-blocking innocent reads.
Test coverage
The new test_require_skill_for_issue_create_gh_api.sh covers the 6 cases enumerated in the issue body's "Test coverage worth carrying" section, mapped 1:1. Sandbox shape mirrors the existing test_require_skill_for_issue_create.sh test. The two existing tests (18/18 unchanged + 6/6 new) give full regression coverage.
Real-world bug confirmation
During this review I tried to read the post-fix hook source via gh api repos/.../contents/...sh to verify the file. The current (pre-merge) main-branch hook blocked the GET with BLOCKED: Raw ticket-create CLI detected (matched pattern: "gh api repos/") — exactly the bug this PR fixes. Live proof the issue is real and the fix is needed.
Issues Found
None.
Verdict
APPROVED
🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 584143789612292cc24f7b7648323870a1b76980
atlas-apex
left a comment
There was a problem hiding this comment.
Code Review: PR #384
Commit: 584143789612292cc24f7b7648323870a1b76980
Summary
Surgical refinement of the gh api repos/ matcher in require-skill-for-issue-create.sh so it only blocks POSTs against /issues endpoints. Previously the pure-substring prefix caught read-only GETs (e.g. gh api repos/o/r/contents/README.md) as if they were ticket-create attempts. Closes #382.
Checklist Results
- Architecture & Design: Pass
- Code Quality: Pass
- Testing: Pass
- Security: Pass (the gate against actual
/issuesPOSTs is preserved; the escape hatch and bootstrap exemption paths remain untouched) - Performance: Pass (one extra
casestatement on a code path that only runs after a match) - PR Description & Glossary: Pass (4 well-explained terms)
- Summary Bullet Narrative: Pass (every bullet has what + why)
- Technical Decisions (AgDR):N/A (surgical bug fix, not a new decision)
- Adopter Handbooks: N/A (no handbooks loaded for shell-script hook fixes)
Diff correctness
- Refinement block is placed exactly where it should be: AFTER
MATCHEDis determined (so we know we matched something) and BEFORE the bootstrap / active-issue-skill exemptions (so a non-/issues-writegh apicall short-circuits cleanly and doesn't waste cycles on exemption resolution). - Case statement is properly scoped:
"gh api"*)is the ONLY arm — all other matched patterns (gh issue create,linear issue create,jira issue create,asana task create) fall through unchanged. Surgical as advertised. - Write-signal enumeration is complete and matches the gh-cli reality:
-X POST/-XPOST/--method POST/--method=POSTcovers explicit method flags (both equals and space forms);-f/-F/--field/--raw-field/--inputcovers field flags that implicitly switch gh from GET to POST. NORM_CMDis whitespace-collapsed viatr -s '[:space:]' ' 'BEFORE the case match, so the pattern's required leading space (*" -f "*etc.) is reliable.- Block-list narrowing (
is_issues_endpoint=1ANDis_write=1both required) preserves the original protection against the/issuesPOST shape while no longer false-blocking innocent reads.
Test coverage
The new test_require_skill_for_issue_create_gh_api.sh covers the 6 cases enumerated in the issue body's "Test coverage worth carrying" section, mapped 1:1:
| Issue body case | Test case | Expected rc |
|---|---|---|
gh api repos/o/r/contents/... GET |
"GET on /contents is no-op (bug fix)" | 0 |
gh api repos/o/r/issues/<id> GET |
"GET on /issues/ is no-op (no write signal)" | 0 |
gh api repos/o/r/pulls -X POST -f ... |
"POST on /pulls is no-op (not an /issues endpoint)" | 0 |
gh api repos/o/r/issues --method POST -f ... blocked |
"POST on /issues via --method is blocked" | 2 |
gh api repos/o/r/issues -f title=x blocked |
"POST on /issues via -f field flag is blocked" | 2 |
gh issue create unaffected |
"gh issue create still blocked (refinement only narrows gh api)" | 2 |
Sandbox shape mirrors the existing test_require_skill_for_issue_create.sh test (per-case sandbox with onboarding.yaml + empty registry + hook + libs, synthetic PreToolUse Bash JSON via jq). The two existing tests (18/18 unchanged + 6/6 new) give full regression coverage.
Real-world bug confirmation
During this review I tried to read the post-fix hook source via gh api repos/.../contents/...sh to verify the file. The current (pre-merge) main-branch hook blocked the GET with BLOCKED: Raw ticket-create CLI detected (matched pattern: "gh api repos/") — exactly the bug this PR fixes. Live proof the issue is real and the fix is needed.
Issues Found
None.
Suggestions
None. The fix is minimal, well-commented (the inline rationale references the issue number directly), and tested.
Verdict
APPROVED (submitted as comment review because self-approve is blocked by GitHub)
🤖 Reviewed by Rex (Code Reviewer Agent)
📌 Reviewed commit: 584143789612292cc24f7b7648323870a1b76980
The pattern `gh api repos/` in ticket.create_command_patterns was a pure substring prefix with no HTTP-method or endpoint refinement, so any read-only `gh api repos/owner/repo/contents/...` GET was blocked as if it were a ticket-create POST. Add a case-match for the `gh api` family after $MATCHED is set: if the command does not BOTH target /issues AND carry a write signal (-X POST / --method POST / -f / -F / --field / --raw-field / --input), downgrade the match to exit 0 (no-op). Other tracker CLIs (gh issue create, linear issue create, jira issue create, asana task create) are unaffected — their patterns stay pure substring prefixes. Regression test added: .claude/hooks/tests/test_require_skill_for_issue_create_gh_api.sh covers GET on /contents, GET on /issues/<id>, POST on /pulls (not /issues), POST on /issues via --method, POST on /issues via field flag, and gh issue create. 6/6 PASS locally. Closes #382 Co-authored-by: me2resh <ahmed.abdelaliem@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
gh api repos/matcher inrequire-skill-for-issue-create.shso it only blocks POST writes against/issuesendpoints — previously any read-onlygh api repos/owner/repo/contents/...GET was caught by the pure substring prefix and blocked with exit 2, which made the hook a routine nuisance for read-only API calls.$MATCHEDis set: if the matched pattern starts withgh api, downgrade to exit 0 unless the command BOTH targets/issuesAND has a write signal (-X POST/--method POST/-f/-F/--field/--raw-field/--input). Placement is intentional — runs only on matched patterns and only refines thegh apicase, so non-issue-create false positives are eliminated without weakening the gate against actual POSTs.gh issue create,linear issue create,jira issue create,asana task createkeep their pure-prefix matching. The refinement targets only the one matcher that had no HTTP-method or endpoint information embedded in its prefix..claude/hooks/tests/test_require_skill_for_issue_create_gh_api.shcovers 6 cases: GET on/contents, GET on/issues/<id>, POST on/pulls(not/issues), POST on/issuesvia--method, POST on/issuesvia field flag, andgh issue create. 6/6 PASS. Existingtest_require_skill_for_issue_create.sh18/18 still PASS — no regression on the non-gh apipatterns.Testing
.claude/hooks/tests/test_require_skill_for_issue_create_gh_api.sh— 6/6 PASS.claude/hooks/tests/test_require_skill_for_issue_create.sh— 18/18 PASS (no regression)gh api repos/o/r/contents/README.mdnow exits 0 (previously exit 2)gh api repos/o/r/issues -f title=xstill exits 2shellcheck -S warning— could not run locally (shellcheck not installed on this machine); CI will exercise itGlossary
ticket.create_command_patterns.claude/project-config.{defaults,}.jsonof CLI command prefixes the hook treats as ticket-create attempts. Adopters extend it for Linear / Jira / Asana / custom trackers.-f/-F/--field)/issues(including/issues,/issues/N,/issues/N/commentsetc.). The hook narrows the block to writes against THIS endpoint specifically./setup,/handover,/update,/split-portfolio) bypass the gate — preserved unchanged by this PR.Closes #382