## #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 file | Target skill | Cases | Validates
--- | --- | --- | ---
`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)