Systems Lab

Agent skill

search-console

Pull Google Search Console data and perform search performance analysis.

activeNeeds a keyActs undeclared1,349 words

Filed under Content and SEO.

From OpenClaudia/openclaudia-skills · 61 skills · 664 · pushed 2026-08-25

What it does when it runs

Pull Google Search Console data and perform search performance analysis. Use when asked about search rankings, clicks, impressions, CTR, index coverage, Core Web Vitals, or sitemap status. Trigger phrases: "search console", "GSC", "search performance", "clicks and impressions", "CTR analysis", "index coverage", "core web vitals", "URL inspection", "sitemap status", "ranking data", "search queries", "keyword positions".

Read from the skill and the 0 files bundled beside it. A skill’s own description is written to be selected by an agent, so it describes the job and not the dependencies.

Keys and connectors you must supply
  • GOOGLE_CLIENT_SECRET
  • GSC_ACCESS_TOKEN
  • REFRESH_TOKEN
Hosts it reaches
  • accounts.google.com
  • oauth2.googleapis.com
  • searchconsole.googleapis.com
  • www.googleapis.com
Tool permissions it declares
No allowed-tools in the frontmatter. It does act, so it runs under whatever permissions your session already grants.
Actions present in the files
shell

Ask about search-console

Opens your assistant with this page's verified links already in the prompt.

Is this safe to install?ClaudeChatGPT
Adapt it to my stackClaudeChatGPT
What else do I need for it to workClaudeChatGPT
Rather ask a human? Talk to Cheetah
git clone --depth 1 --filter=blob:none --sparse https://github.com/OpenClaudia/openclaudia-skills.git /tmp/openclaudia-skills
git -C /tmp/openclaudia-skills sparse-checkout set "skills/search-console"
mkdir -p ~/.claude/skills/search-console
cp -R "/tmp/openclaudia-skills/skills/search-console/." ~/.claude/skills/search-console/

Picked up without a restart. A project skill of the same name is shadowed by your personal one. For one repository only, swap ~/.claude/skills for .claude/skills. Claude Code docs ↗

The folder is the same in every client that implements the format — 46 of them — so if yours is not above, only the destination changes.

Before you install: this skill will not complete its job on a bare agent. It needs GOOGLE_CLIENT_SECRET, GSC_ACCESS_TOKEN, REFRESH_TOKEN, which you have to obtain separately.

Reproduced in full from OpenClaudia/openclaudia-skills/blob/221b37d7ab95c14d5343c7b24fd9f9367a3fb400/skills/search-console/SKILL.md, which is licensed MIT (repository). 1,349 words, 41 headings.

Google Search Console

Pull search performance data, index coverage, and Core Web Vitals from Google Search Console API.

Prerequisites

Requires Google OAuth credentials:

  • GOOGLE_CLIENT_ID
  • GOOGLE_CLIENT_SECRET
  • A valid OAuth access token with https://www.googleapis.com/auth/webmasters.readonly scope

Set credentials in .env, .env.local, or ~/.claude/.env.global.

Getting an Access Token

# Step 1: Authorization URL (user visits in browser)
echo "https://accounts.google.com/o/oauth2/v2/auth?client_id=${GOOGLE_CLIENT_ID}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/webmasters.readonly&response_type=code&access_type=offline"

# Step 2: Exchange code for tokens
curl -s -X POST "https://oauth2.googleapis.com/token" \
  -d "code={AUTH_CODE}" \
  -d "client_id=${GOOGLE_CLIENT_ID}" \
  -d "client_secret=${GOOGLE_CLIENT_SECRET}" \
  -d "redirect_uri=urn:ietf:wg:oauth:2.0:oob" \
  -d "grant_type=authorization_code"

# Step 3: Refresh expired token
curl -s -X POST "https://oauth2.googleapis.com/token" \
  -d "refresh_token={REFRESH_TOKEN}" \
  -d "client_id=${GOOGLE_CLIENT_ID}" \
  -d "client_secret=${GOOGLE_CLIENT_SECRET}" \
  -d "grant_type=refresh_token"

Listing Available Sites

curl -s -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  "https://www.googleapis.com/webmasters/v3/sites" \
  | python3 -c "
import json, sys
data = json.load(sys.stdin)
for site in data.get('siteEntry', []):
    print(f\"{site['siteUrl']}  |  Permission: {site['permissionLevel']}\")
"

The site URL format is either https://example.com/ (URL prefix) or sc-domain:example.com (domain property).


1. Search Performance Report

The core report: queries, pages, clicks, impressions, CTR, and average position.

API Endpoint

POST https://www.googleapis.com/webmasters/v3/sites/{siteUrl}/searchAnalytics/query

Note: The {siteUrl} must be URL-encoded (e.g., https%3A%2F%2Fexample.com%2F or sc-domain%3Aexample.com).

Top Queries

curl -s -X POST \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2024-01-01",
    "endDate": "2024-03-31",
    "dimensions": ["query"],
    "rowLimit": 50,
    "startRow": 0
  }'

Top Pages

curl -s -X POST \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2024-01-01",
    "endDate": "2024-03-31",
    "dimensions": ["page"],
    "rowLimit": 50
  }'

Query + Page Combination

curl -s -X POST \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2024-01-01",
    "endDate": "2024-03-31",
    "dimensions": ["query", "page"],
    "rowLimit": 100,
    "dimensionFilterGroups": [{
      "filters": [{
        "dimension": "page",
        "operator": "contains",
        "expression": "/blog/"
      }]
    }]
  }'

Available Dimensions

DimensionDescription
querySearch query
pageURL
countryCountry code (ISO 3166-1 alpha-3)
deviceDESKTOP, MOBILE, TABLET
dateIndividual date
searchAppearanceRich result type

Response Parsing

curl -s -X POST "..." | python3 -c "
import json, sys
data = json.load(sys.stdin)
print(f\"{'Query':<50} {'Clicks':>8} {'Impr':>8} {'CTR':>8} {'Pos':>6}\")
print('-' * 82)
for row in data.get('rows', []):
    keys = ' + '.join(row.get('keys', []))
    print(f\"{keys:<50} {row['clicks']:>8} {row['impressions']:>8} {row['ctr']*100:>7.1f}% {row['position']:>6.1f}\")
"

2. Search Performance by Date

Track daily trends for queries and pages.

curl -s -X POST \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2024-01-01",
    "endDate": "2024-03-31",
    "dimensions": ["date"],
    "rowLimit": 1000
  }'

To track a specific query over time:

curl -s -X POST \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2024-01-01",
    "endDate": "2024-03-31",
    "dimensions": ["date"],
    "dimensionFilterGroups": [{
      "filters": [{
        "dimension": "query",
        "operator": "equals",
        "expression": "your target keyword"
      }]
    }]
  }'

3. Index Coverage (URL Inspection API)

Check if a specific URL is indexed.

Endpoint

POST https://searchconsole.googleapis.com/v1/urlInspection/index:inspect

Example curl

curl -s -X POST \
  "https://searchconsole.googleapis.com/v1/urlInspection/index:inspect" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "inspectionUrl": "https://example.com/page-to-check",
    "siteUrl": "sc-domain:example.com"
  }'

Response Fields

FieldDescription
inspectionResult.indexStatusResult.coverageStateSubmitted and indexed, Crawled - currently not indexed, etc.
inspectionResult.indexStatusResult.robotsTxtStateALLOWED or DISALLOWED
inspectionResult.indexStatusResult.indexingStateINDEXING_ALLOWED or INDEXING_NOT_ALLOWED
inspectionResult.indexStatusResult.lastCrawlTimeWhen Googlebot last crawled
inspectionResult.indexStatusResult.crawledAsDESKTOP or MOBILE
inspectionResult.mobileUsabilityResult.verdictPASS, FAIL, or VERDICT_UNSPECIFIED

4. Sitemaps

List and check sitemap status.

List Sitemaps

curl -s -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/sitemaps" \
  | python3 -c "
import json, sys
data = json.load(sys.stdin)
for sm in data.get('sitemap', []):
    print(f\"URL: {sm['path']}\")
    print(f\"  Type: {sm.get('type','')}  |  Submitted: {sm.get('lastSubmitted','')}\")
    print(f\"  URLs discovered: {sm.get('contents',[{}])[0].get('submitted','?')}  |  Indexed: {sm.get('contents',[{}])[0].get('indexed','?')}\")
    print()
"

Submit a Sitemap

curl -s -X PUT -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/sitemaps/https%3A%2F%2Fexample.com%2Fsitemap.xml"

5. Opportunity Identification

Use Search Console data to find SEO opportunities.

Low-Hanging Fruit: High Impressions, Low CTR

Queries with many impressions but low CTR suggest the title/description needs optimization.

# Pull queries, then filter for: impressions > 100 AND ctr < 0.03 AND position < 20
curl -s -X POST \
  "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Aexample.com/searchAnalytics/query" \
  -H "Authorization: Bearer ${GSC_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "startDate": "2024-01-01",
    "endDate": "2024-03-31",
    "dimensions": ["query", "page"],
    "rowLimit": 1000
  }' | python3 -c "
import json, sys
data = json.load(sys.stdin)
print('== Low CTR Opportunities (High impressions, low CTR, good position) ==')
print(f\"{'Query':<40} {'Page':<40} {'Impr':>6} {'CTR':>7} {'Pos':>5}\")
for row in data.get('rows', []):
    if row['impressions'] > 100 and row['ctr'] < 0.03 and row['position'] < 20:
        print(f\"{row['keys'][0]:<40} {row['keys'][1][-40:]:<40} {row['impressions']:>6} {row['ctr']*100:>6.1f}% {row['position']:>5.1f}\")
"

Striking Distance: Position 5-20

Queries ranking on page 1-2 that could be pushed to top 5 with content optimization.

# Filter for position between 5 and 20 with decent impressions
curl -s -X POST "..." | python3 -c "
import json, sys
data = json.load(sys.stdin)
print('== Striking Distance Keywords (Position 5-20) ==')
opps = [r for r in data.get('rows',[]) if 5 <= r['position'] <= 20 and r['impressions'] > 50]
opps.sort(key=lambda x: x['impressions'], reverse=True)
for row in opps[:30]:
    print(f\"{row['keys'][0]:<50} Pos: {row['position']:>5.1f}  Impr: {row['impressions']:>6}  Clicks: {row['clicks']:>4}\")
"

Cannibalization Detection

Find queries where multiple pages compete for the same keyword.

# Pull query+page data, then group by query to find duplicates
curl -s -X POST "..." | python3 -c "
import json, sys
from collections import defaultdict
data = json.load(sys.stdin)
query_pages = defaultdict(list)
for row in data.get('rows', []):
    query_pages[row['keys'][0]].append({
        'page': row['keys'][1],
        'clicks': row['clicks'],
        'impressions': row['impressions'],
        'position': row['position']
    })
print('== Keyword Cannibalization (multiple pages for same query) ==')
for query, pages in sorted(query_pages.items(), key=lambda x: -sum(p['impressions'] for p in x[1])):
    if len(pages) > 1:
        total_impr = sum(p['impressions'] for p in pages)
        if total_impr > 100:
            print(f\"\nQuery: {query} ({total_impr} total impressions)\")
            for p in sorted(pages, key=lambda x: -x['impressions']):
                print(f\"  {p['page'][-60:]}  Pos: {p['position']:.1f}  Impr: {p['impressions']}  Clicks: {p['clicks']}\")
"

Workflow: Full Search Performance Audit

When asked for a complete GSC audit:

  1. Overall Metrics: Total clicks, impressions, avg CTR, avg position for last 90 days vs previous 90 days
  2. Top 30 Queries: By clicks, with CTR and position
  3. Top 20 Pages: By clicks, with CTR and position
  4. Device Breakdown: Desktop vs mobile performance
  5. Low-Hanging Fruit: High impressions + low CTR opportunities
  6. Striking Distance: Position 5-20 keywords with optimization potential
  7. Cannibalization: Queries with multiple competing pages
  8. Index Coverage: Spot-check important URLs
  9. Sitemap Health: Verify sitemaps are submitted and indexed

Report Format

## Search Console Audit: {domain}
### Period: {date range}

### Summary
| Metric | Current | Previous | Change |
|--------|---------|----------|--------|
| Clicks | X | Y | +Z% |
| Impressions | X | Y | +Z% |
| Avg CTR | X% | Y% | +Z pp |
| Avg Position | X | Y | +Z |

### Top Queries
| Query | Clicks | Impressions | CTR | Position |
|-------|--------|-------------|-----|----------|
| ...   | ...    | ...         | ... | ...      |

### Optimization Opportunities

#### Title/Description Optimization (High Impressions, Low CTR)
1. "{query}" - {impressions} impressions, {ctr}% CTR, position {pos}
   - Page: {url}
   - Recommendation: ...

#### Content Optimization (Striking Distance)
1. "{query}" - position {pos}, {impressions} impressions
   - Action: Add {query} to H2, expand section on {topic}

#### Cannibalization Fixes
1. "{query}" appears on {n} pages
   - Consolidate to: {best_url}
   - Redirect/noindex: {other_urls}

Rate Limits

  • Search Analytics API: 1,200 queries per minute
  • URL Inspection API: 2,000 inspections per day per property
  • Data freshness: Search data is typically 2-3 days behind

Common Errors

ErrorCauseFix
403No access to this propertyVerify ownership in GSC
400Invalid date rangeDates must be within last 16 months
Empty rowsNo data matching filtersBroaden date range or remove filters

Other skills for the same job

Different authors, same problem. Matched on the words in the skill name, across every library in the catalogue except this one.

Need help setting it up?

This page tells you what search-console does and what it needs. Cheetah builds the agent setup it runs inside: data, CRM, sequencing and the guardrails.

Book a call →

The directory stays free. There is nothing gated behind this.