CC 2.1.116 follow-ups — PostToolUse gh rate-limit hook + skill eval coverage

Closes #1435 (belt-and-suspenders enforcement on top of PR #1434's skill-level gh rate-limit guidance) and #1436 (5 eval cases verifying the model applies the 2.1.116 behavioral knowledge).

#1435 — PostToolUse hook: posttool/bash/gh-rate-limit-tracker

What it does

Fires after every Bash tool call. Short-circuits if the command isn't a gh invocation (handles bare gh, absolute paths, env-var prefixes). Scans combined stdout+stderr for rate-limit indicators, injects additionalContext telling the model to stop.

// Detection — any one triggers
/API rate limit exceeded/i                // primary hourly limit
/exceeded a secondary rate limit/i        // short-burst limit
/HTTP\/[0-9.]+\s+403/i + /rate limit/i    // 403 + rate phrase (false-positive guarded)

// Optional reset-time extraction
/reset(?:\s+at)?:?\s*([0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9:]+\s*(?:UTC|Z)?)/i

Output shape

[gh-rate-limit-tracker] GitHub API rate limit detected

**Do not retry this gh command.**

The hourly limit resets at 2026-04-21 10:00:00 UTC. Wait until that time before resuming gh calls.

If this call was part of a loop (/loop 5m gh pr checks ...), stop the loop — do not let it continue firing into the rate limit.

False-positive guard

Plain HTTP 403 (permission denied, resource not accessible) does not trigger. Only 403 combined with "rate limit" phrasing in the body. Verified in tests:

test('clean 403 permission error does NOT trigger', ...)  ✓ pass

#1436 — Skill eval coverage (5 files, 9 eval cases)

Eval fileTarget skillCasesValidates
github-operations-rate-limit.eval.yaml github-operations 2 Mid-bulk-op 403 → stop iteration, wait for reset, no token rotation bypass. Pre-flight pattern guidance.
create-pr-rate-limit.eval.yaml create-pr 2 /loop gh pr checks backs off (does NOT tighten interval). Secondary limit handled differently from primary.
review-pr-rate-limit.eval.yaml review-pr 1 Phase 1 mid-op 403 → pause OR fall back to local git diff. Preserves Phase 1 partial state. Doesn't launch Phase 3 with incomplete context.
doctor-reload-plugins-auto-deps.eval.yaml doctor 2 CC 2.1.116+ missing-dep → /reload-plugins first (not plugin install). Pre-2.1.116 → manual install with upgrade note.
agent-hooks-main-thread.eval.yaml src/agents/README.md → .claude/rules/agent-authoring.md 2 New agent hooks must be context-agnostic. Skeleton generator does NOT branch on "am I a subagent?". Uses CLAUDE_AGENT_ID for logging only.

Files touched

New files (6)

  • src/hooks/src/posttool/bash/gh-rate-limit-tracker.ts (~130 lines)
  • src/hooks/src/__tests__/posttool/bash/gh-rate-limit-tracker.test.ts (19 tests)
  • 5 × tests/evals/skills/*.eval.yaml (9 eval cases total)

Registered (4)

  • src/hooks/hooks.json — PostToolUse/Bash entry (async:true, 5s timeout)
  • src/hooks/src/entries/posttool.ts — import + registry entry
  • CLAUDE.md — hook count 180→181, global 112→113
  • hooks.json.description — 180 total → 181 total

Test count adjustments (4 files)

  • split-bundles.test.ts: totalHooks 210→211
  • async-registry.test.ts: asyncHooks 76→77
  • dispatcher-registry-wiring.test.ts (×2): 76→77
  • hooks-json-wiring.test.ts: "180 total" → "181 total"

Test plan

$ cd src/hooks && npx vitest run   7761/7761 pass
$ cd src/hooks && npx tsc --noEmit  clean
$ npm run build                     ✓ 338 KB unified bundle
$ npm run test:skills               106 skills pass
$ npm run test:agents               37 agents pass
$ npm run test:security             13/13
$ npm run test:manifests            106/106 delivered

# eval YAML parse check
$ python3 -c "import yaml;[yaml.safe_load(open(f)) for f in glob(...)]"  5/5 valid

Bundle impact

posttool.mjs:  84,115 bytes  →  85,947 bytes   (+1,832 = +2.2%)
hooks.mjs:     338.05 KB     →  338.05 KB      (unchanged — gh-rate-limit-tracker
                                                is small enough to fit in existing
                                                padding / similar symbols collapse)