@zuppif/gsc-cli
v0.1.1
Published
Agent-friendly Google Search Console CLI
Maintainers
Readme
gsc-cli
Agent-friendly Google Search Console CLI.
gsc-cli wraps the Google Search Console API with a strict command contract:
gsc-cli <resource> <action> [params]Stdout is always JSON. There is no --json, table output, color output, file output, pager, browser login, or interactive prompt. The CLI is meant to be easy for agents, scripts, CI jobs, and humans to call without guessing output shape.
Install
Global install:
npm install -g @zuppif/gsc-cli
gsc-cli --helpOne-off run:
npx @zuppif/gsc-cli sites listFrom this checkout:
bun install
bun run build
bun run dev --helpLocal editable install with Bun:
cd /path/to/gsc-cli
bun link
gsc-cli --helpIf another project needs the package as a dependency while you work on it:
cd /path/to/gsc-cli
bun link
cd /path/to/other-project
bun link @zuppif/gsc-cliGet Google Credentials
The recommended setup for agents is a Google Cloud service account with a JSON key, added as a user to the target Search Console property.
Important distinction: Google Cloud IAM access and Search Console property access are different. Giving a service account a Google Cloud role does not give it Search Console data. The service account email must also be added to the Search Console property.
Do not use a Google API key for this CLI. Search Console data access needs OAuth credentials with Search Console scopes plus property-level permission. For unattended CLI and agent workflows, use a service account JSON key.
1. Create or Choose a Google Cloud Project
Open Google Cloud Console:
https://console.cloud.google.com/Create a project or select an existing project that will own the service account.
2. Enable the Search Console API
Enable the Google Search Console API for the project:
https://console.developers.google.com/apis/api/searchconsole.googleapis.com/overviewYou can also find it in Google Cloud Console:
APIs & Services -> Library -> Google Search Console API -> Enable3. Create a Service Account
Open:
IAM & Admin -> Service Accounts -> Create service accountUse a clear name, for example:
gsc-cli-agentUse this description:
Read-only Google Search Console access for gsc-cli reporting.After creation, copy the service account email. It looks like this:
gsc-cli-agent@<project-id>.iam.gserviceaccount.comGoogle Cloud project permissions: none needed beyond basic service account existence for Search Console reads. The real permission comes from adding that service account email inside Search Console in step 5.
4. Create a JSON Key
Open the new service account:
Service Accounts -> gsc-cli-agent -> three-dot menu -> Manage keys -> Add key -> Create new key -> JSONDownload the JSON file and store it somewhere stable:
mkdir -p "$HOME/.gsc/accounts"
mv "<PATH TO STORED JSON>" "$HOME/.gsc/accounts/gsc-cli-agent.json"
chmod 600 "$HOME/.gsc/accounts/gsc-cli-agent.json"Do not commit this file. It contains a private key.
5. Add the Service Account to Search Console
Open Google Search Console:
https://search.google.com/search-consoleSelect the exact property you want the CLI to access, then:
Settings -> Users and permissions -> Add userPaste the service account email:
gsc-cli-agent@<project-id>.iam.gserviceaccount.comChoose the minimum permission that can do the job:
| Permission | Use it for | | --- | --- | | Restricted | Read-only reporting workflows such as Search Analytics and site listing. Start here when possible. | | Full | URL inspection and sitemap workflows when Restricted is not enough. Also useful for agents that need broader operational access. | | Owner | Only when you intentionally want the account to manage verification, settings, or users. Avoid this for basic agent work. |
Start with Restricted user. That should be enough for Search Analytics reads. If URL Inspection or sitemap actions fail later, bump the service account to Full, but start restricted.
6. Point gsc-cli at the Key
The clean default is GOOGLE_APPLICATION_CREDENTIALS:
export GOOGLE_APPLICATION_CREDENTIALS="$HOME/.gsc/accounts/gsc-cli-agent.json"
gsc-cli auth statusYou can also put it in a local .env file:
GOOGLE_APPLICATION_CREDENTIALS=/Users/you/.gsc/accounts/gsc-cli-agent.json
GSC_SITE_URL=sc-domain:example.comOr pass the key per command:
gsc-cli --credentials "$HOME/.gsc/accounts/gsc-cli-agent.json" sites list
gsc-cli sites list --credentials "$HOME/.gsc/accounts/gsc-cli-agent.json"Global flags work before or after the subcommand.
7. Verify Access
Check that credentials load:
gsc-cli auth status --prettyExpected shape:
{
"ok": true,
"command": "auth.status",
"data": {
"authenticated": true,
"source": "GOOGLE_APPLICATION_CREDENTIALS",
"credentials": "/Users/you/.gsc/accounts/gsc-cli-agent.json"
},
"meta": {
"elapsedMs": 123
}
}Check that Search Console property access works:
gsc-cli sites list --prettyExample:
{
"ok": true,
"command": "sites.list",
"data": {
"siteEntry": [
{
"siteUrl": "sc-domain:example.com",
"permissionLevel": "siteFullUser"
}
]
},
"meta": {
"elapsedMs": 292
}
}Quick Start
export GOOGLE_APPLICATION_CREDENTIALS="$HOME/.gsc/accounts/gsc-cli-agent.json"
gsc-cli auth status
gsc-cli sites list
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions page \
--type web \
--limit 25 \
--prettySite URL Formats
Use the property string exactly as Search Console knows it.
Domain property:
sc-domain:example.comURL-prefix property:
https://www.example.com/URL-prefix properties must include the protocol and trailing slash. A property for https://www.example.com/ is not the same as https://example.com/, http://www.example.com/, or sc-domain:example.com.
The safest flow is:
gsc-cli sites listCopy the exact siteUrl value from the JSON output and reuse it in later commands.
Global Options
These options are accepted by every command:
| Option | Type | Description |
| --- | --- | --- |
| --credentials <path> | string | Path to a service account JSON key. Overrides ambient Google auth for the command. |
| --timeout-ms <number> | integer | Google API timeout in milliseconds. Must be >= 1. |
| --pretty | boolean | Pretty-print the JSON envelope. Default output is compact JSON. |
Examples:
gsc-cli --pretty sites list
gsc-cli sites list --pretty
gsc-cli --credentials "$HOME/.gsc/accounts/gsc-cli-agent.json" sites list
gsc-cli sites list --credentials "$HOME/.gsc/accounts/gsc-cli-agent.json"
gsc-cli analytics query --timeout-ms 30000 --site "sc-domain:example.com" --start 2026-06-01 --end 2026-06-12Output Contract
Success:
{
"ok": true,
"command": "analytics.query",
"data": {},
"meta": {
"elapsedMs": 123,
"siteUrl": "sc-domain:example.com"
}
}Failure:
{
"ok": false,
"command": "analytics.query",
"error": {
"code": "MISSING_SITE",
"message": "Missing required --site"
}
}Command outputs are always written to stdout. Failed commands exit non-zero.
Known error codes from local validation include:
| Code | Meaning |
| --- | --- |
| MISSING_SITE | --site is required. |
| MISSING_URL | --url is required. |
| MISSING_SITEMAP | --sitemap is required. |
| MISSING_START | --start is required in analytics flag mode. |
| MISSING_END | --end is required in analytics flag mode. |
| INVALID_ARG | A value is not one of the accepted options. |
| INVALID_NUMBER | A numeric flag is not an integer or is outside the accepted range. |
| INVALID_BODY | --body is not valid JSON object input. |
| AMBIGUOUS_BODY | --body was mixed with request-building flags. |
| INVALID_DIMENSION | Analytics dimension is not accepted by this CLI. |
| INVALID_FILTER | Analytics filter syntax, dimension, or operator is invalid. |
| GOOGLE_API_ERROR | Google API or auth layer rejected the request. |
Commands
auth status
Validate that authentication can create a Google API client.
gsc-cli auth status
gsc-cli auth status --credentials "$HOME/.gsc/accounts/gsc-cli-agent.json" --prettyReturns:
{
"authenticated": true,
"source": "credentials",
"credentials": "/Users/you/.gsc/accounts/gsc-cli-agent.json"
}source is one of:
| Source | Meaning |
| --- | --- |
| credentials | --credentials was passed. |
| GOOGLE_APPLICATION_CREDENTIALS | The environment variable is set. |
| application-default | Google Application Default Credentials were used. |
sites list
List Search Console properties visible to the authenticated account.
gsc-cli sites list
gsc-cli sites list --prettyUse this command first. It tells you which siteUrl strings the authenticated identity can access.
sites get
Get one Search Console property.
gsc-cli sites get --site "sc-domain:example.com"
gsc-cli sites get --site "https://www.example.com/"Required:
| Option | Description |
| --- | --- |
| --site <siteUrl> | Exact Search Console property string. |
analytics query
Query Search Analytics performance data.
There are two modes:
| Mode | Use it when | | --- | --- | | Flag mode | You want the CLI to build the Google request body from simple flags. | | Raw body mode | You need the full Google request body, multiple filter groups, future API fields, or exact control. |
Do not mix --body with request-building flags. The CLI will fail with AMBIGUOUS_BODY.
Flag Mode
Minimum query:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12Top page metrics:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions page \
--type web \
--limit 25 \
--prettyPage and query drilldown:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions page,query \
--type web \
--limit 1000Queries for one page:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions query \
--filter "page:equals:https://example.com/pricing" \
--limit 100Brand/non-brand split with regex:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions query \
--filter "query:excludingRegex:(?i).*example.*" \
--limit 1000Device split:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions device,page \
--limit 500Daily trend:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions date \
--data-state finalFresh data:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-14 \
--end 2026-06-15 \
--dimensions date \
--data-state allHourly fresh data:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-15 \
--end 2026-06-15 \
--dimensions hour \
--data-state hourly_allPagination:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions page,query \
--limit 25000 \
--start-row 0
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions page,query \
--limit 25000 \
--start-row 25000Google sorts most Search Analytics results by clicks descending. If you need "top by impressions", fetch the rows and sort the returned JSON downstream by impressions.
Flag reference:
| Option | Required | Values |
| --- | --- | --- |
| --site <siteUrl> | yes | Exact Search Console property string. |
| --start <YYYY-MM-DD> | yes in flag mode | Inclusive start date in Google Search Console's reporting timezone. |
| --end <YYYY-MM-DD> | yes in flag mode | Inclusive end date. |
| --dimensions <csv> | no | Any comma-separated subset of country, device, page, query, searchAppearance, date, hour. |
| --type <type> | no | web, image, video, news, discover, googleNews. |
| --data-state <state> | no | final, all, hourly_all. |
| --aggregation <type> | no | auto, byPage, byProperty, byNewsShowcasePanel. |
| --limit <number> | no | Integer from 1 to 25000. Maps to rowLimit. |
| --start-row <number> | no | Integer >= 0. Maps to startRow. |
| --filter <filter> | no | One filter as dimension:operator:expression. |
| --body <json-or-> | no | Raw Google request body JSON object, or - to read JSON from stdin. |
Filter syntax:
dimension:operator:expressionFilter dimensions:
country
device
page
query
searchAppearanceFilter operators:
contains
equals
notContains
notEquals
includingRegex
excludingRegexExamples:
--filter "country:equals:usa"
--filter "device:equals:MOBILE"
--filter "page:contains:/blog/"
--filter "query:includingRegex:(?i).*pricing.*"For multiple filters or multiple filter groups, use raw body mode.
Raw Body Mode
Pass the Google Search Analytics request body directly:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--body '{"startDate":"2026-06-01","endDate":"2026-06-12","dimensions":["page"],"rowLimit":10}'Read the request body from stdin:
printf '%s\n' '{"startDate":"2026-06-01","endDate":"2026-06-12","dimensions":["query"],"rowLimit":10}' \
| gsc-cli analytics query --site "sc-domain:example.com" --body -Multiple filters:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--body '{
"startDate": "2026-06-01",
"endDate": "2026-06-12",
"dimensions": ["page", "query"],
"dimensionFilterGroups": [
{
"groupType": "and",
"filters": [
{ "dimension": "page", "operator": "contains", "expression": "/blog/" },
{ "dimension": "query", "operator": "notContains", "expression": "example" }
]
}
],
"rowLimit": 1000
}'Raw body fields supported by Google:
| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| startDate | string | yes | Inclusive date, YYYY-MM-DD. |
| endDate | string | yes | Inclusive date, YYYY-MM-DD. |
| dimensions | string array | no | Groups rows. Common values: country, device, page, query, searchAppearance, date, hour. |
| type | string | no | web, image, video, news, discover, googleNews. searchType is deprecated by Google; use type. |
| dimensionFilterGroups | array | no | Filter groups. Google currently supports groupType: "and". |
| dimensionFilterGroups[].groupType | string | no | Use and. |
| dimensionFilterGroups[].filters[].dimension | string | no | country, device, page, query, searchAppearance. |
| dimensionFilterGroups[].filters[].operator | string | no | contains, equals, notContains, notEquals, includingRegex, excludingRegex. |
| dimensionFilterGroups[].filters[].expression | string | no | Filter expression. Device values are usually DESKTOP, MOBILE, TABLET; countries use 3-letter country codes. |
| aggregationType | string | no | auto, byPage, byProperty, byNewsShowcasePanel. |
| rowLimit | integer | no | 1 to 25000; Google default is 1000. |
| startRow | integer | no | Zero-based row offset for pagination. |
| dataState | string | no | final, all, hourly_all. |
The CLI only validates that --body is a JSON object. It passes the object through to Google, so new API fields can be used before this README is updated.
Search Analytics response shape:
{
"rows": [
{
"keys": ["https://example.com/pricing", "pricing software"],
"clicks": 12,
"impressions": 340,
"ctr": 0.03529411764705882,
"position": 6.7
}
],
"responseAggregationType": "byPage"
}url inspect
Inspect the indexed status of one URL.
gsc-cli url inspect \
--site "sc-domain:example.com" \
--url "https://example.com/pricing" \
--language-code en-US \
--prettyRequired:
| Option | Description |
| --- | --- |
| --site <siteUrl> | Exact Search Console property string. |
| --url <url> | Fully-qualified URL to inspect. Must be under the property in --site. |
Optional:
| Option | Default | Description |
| --- | --- | --- |
| --language-code <code> | en-US | BCP-47 language code for translated issue messages. |
Google's URL Inspection API returns the indexed version known to Google. It does not run a live URL test.
sitemaps list
List submitted sitemaps for a site.
gsc-cli sitemaps list --site "sc-domain:example.com"With a sitemap index:
gsc-cli sitemaps list \
--site "sc-domain:example.com" \
--sitemap-index "https://example.com/sitemap_index.xml"Required:
| Option | Description |
| --- | --- |
| --site <siteUrl> | Exact Search Console property string. |
Optional:
| Option | Description |
| --- | --- |
| --sitemap-index <url> | List sitemap entries included in a sitemap index. |
sitemaps get
Get one sitemap.
gsc-cli sitemaps get \
--site "sc-domain:example.com" \
--sitemap "https://example.com/sitemap.xml"Required:
| Option | Description |
| --- | --- |
| --site <siteUrl> | Exact Search Console property string. |
| --sitemap <url> | Sitemap URL. |
sitemaps submit
Submit a sitemap.
gsc-cli sitemaps submit \
--site "sc-domain:example.com" \
--sitemap "https://example.com/sitemap.xml"Returns data: null on success.
This command uses the write OAuth scope:
https://www.googleapis.com/auth/webmastersThe service account must have enough Search Console permission for the property.
sitemaps delete
Delete a submitted sitemap from Search Console.
gsc-cli sitemaps delete \
--site "sc-domain:example.com" \
--sitemap "https://example.com/sitemap.xml"Returns data: null on success.
This removes the sitemap submission from Search Console. It does not delete the sitemap file from your website.
Common Agent Workflows
Find accessible properties:
gsc-cli sites listFetch page metrics for a date range:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions page \
--limit 25000Fetch query metrics for one page:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions query \
--filter "page:equals:https://example.com/pricing" \
--limit 1000Fetch page-query pairs:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions page,query \
--limit 25000Fetch country and device mix:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions country,device \
--type webCompare Discover separately from web search:
gsc-cli analytics query \
--site "sc-domain:example.com" \
--start 2026-06-01 \
--end 2026-06-12 \
--dimensions page \
--type discover \
--limit 1000Inspect a URL after an indexing issue:
gsc-cli url inspect \
--site "sc-domain:example.com" \
--url "https://example.com/pricing"Check submitted sitemaps:
gsc-cli sitemaps list --site "sc-domain:example.com"Submit a sitemap after publishing:
gsc-cli sitemaps submit \
--site "sc-domain:example.com" \
--sitemap "https://example.com/sitemap.xml"Troubleshooting
auth status fails with ENOENT
The key path is wrong.
ls -l "$HOME/.gsc/accounts/gsc-cli-agent.json"
gsc-cli auth status --credentials "$HOME/.gsc/accounts/gsc-cli-agent.json"auth status works but sites list is empty
Authentication works, but the authenticated identity has no Search Console properties.
Fix:
- Copy the
client_emailfrom the JSON key. - Open the Search Console property.
- Add that email under
Settings -> Users and permissions. - Run
gsc-cli sites listagain.
403 or insufficient permissions
Common causes:
| Cause | Fix |
| --- | --- |
| Search Console API is not enabled in the Google Cloud project. | Enable the Search Console API for the project that owns the key. |
| Service account was not added to Search Console. | Add the client_email from the key as a Search Console user. |
| Wrong property string. | Copy the exact siteUrl from gsc-cli sites list. |
| Write command with read-only access. | Use a Search Console account with enough permission or raise the service account permission. |
| Wrong key file. | Run gsc-cli auth status --pretty and check the credential source. |
sites get fails but sites list shows properties
You are probably passing a different property string than Search Console expects. Use the exact siteUrl value from sites list.
Good:
gsc-cli sites get --site "sc-domain:example.com"
gsc-cli sites get --site "https://www.example.com/"Common mistakes:
gsc-cli sites get --site "example.com"
gsc-cli sites get --site "https://www.example.com"
gsc-cli sites get --site "http://www.example.com/"analytics query returns no rows
Check:
| Check | Why it matters |
| --- | --- |
| Date range | Search Console data may not exist for the requested dates. |
| Property string | sc-domain:example.com and https://www.example.com/ can expose different scopes. |
| Search type | web, discover, googleNews, news, image, and video are separate. |
| Filters | A strict page/query/device/country filter can eliminate all rows. |
| Data freshness | Use --data-state all for recent partial data, or final for stable data. |
Need more than 25000 rows
Use pagination with --start-row.
gsc-cli analytics query --site "sc-domain:example.com" --start 2026-06-01 --end 2026-06-12 --dimensions page,query --limit 25000 --start-row 0
gsc-cli analytics query --site "sc-domain:example.com" --start 2026-06-01 --end 2026-06-12 --dimensions page,query --limit 25000 --start-row 25000
gsc-cli analytics query --site "sc-domain:example.com" --start 2026-06-01 --end 2026-06-12 --dimensions page,query --limit 25000 --start-row 50000Stop when the returned rows array is missing or empty.
Need multiple filters
Use --body.
gsc-cli analytics query \
--site "sc-domain:example.com" \
--body '{"startDate":"2026-06-01","endDate":"2026-06-12","dimensions":["page","query"],"dimensionFilterGroups":[{"groupType":"and","filters":[{"dimension":"page","operator":"contains","expression":"/blog/"},{"dimension":"query","operator":"notContains","expression":"example"}]}],"rowLimit":1000}'Need stable JSON for another tool
Default output is compact JSON:
gsc-cli sites listUse --pretty only for humans:
gsc-cli sites list --prettyOfficial API References
- Google service account credentials:
https://developers.google.com/workspace/guides/create-credentials#service-account - Google Cloud service accounts:
https://docs.cloud.google.com/iam/docs/service-accounts-create - Search Console users and permissions:
https://support.google.com/webmasters/answer/7687615 - Search Console API reference:
https://developers.google.com/webmaster-tools/v1/api_reference_index - Search Analytics query:
https://developers.google.com/webmaster-tools/v1/searchanalytics/query - URL Inspection inspect:
https://developers.google.com/webmaster-tools/v1/urlInspection.index/inspect - Sitemaps API:
https://developers.google.com/webmaster-tools/v1/sitemaps
Development
bun install
bun run dev --help
bun run check
bun run buildBefore publishing, verify the package contents:
npm pack --dry-runPublish
bun run check
bun run build
npm publish --access public