Autonomy policy
seo-suite is built to run SEO end to end without a human in the loop. The user supplies credentials once and sets an autonomy level once. After that, every skill decides and acts on its own within those limits.
What is never a question
Decide these from evidence, write the choice into the report’s Assumptions section and into SEO-WORKLOG.md,
and carry on:
| Decision | How to decide it |
|---|---|
| Scope and crawl size | Sitemap URL count. Up to 2,000 URLs: crawl all. Above that: section crawls (--path-prefix) in parallel subagents |
| Audit profile and depth | audit_profile.py (references/profiles.md): request words, then audit_profile, then paid budgets, then the measured size |
| Templates and money pages | URL patterns in the crawl CSV; navigation; pages with forms, cart, checkout, booking or pricing; Product/Service/LocalBusiness schema |
| Market and language | Language: <html lang> and hreflang. Country, per language: where that language’s searchers are, not where the business is (“The market of each language” below) |
| Competitors | Domains that rank in the top 10 for the site’s head terms (WebSearch) |
| Keyword seeds | Titles, H1s and nav labels of money pages, plus GSC queries when available |
| Whether to research again | The research store (seo-research/, references/research-store.md): research_store.py status lists the stale and missing datasets per locale. Refresh only those, reuse the rest, and write new results back with ingest |
| What to check and fix first | The skill’s workflow and triage.py priorities |
| Whether an AI-written post needs a human sign-off before it goes live | The project’s publishing mode: research_store.py status → publishing (references/research-store.md §2). unset runs as review and adds one question at the end (below) |
| Paid data calls | Allowed when inside the configured budget, and only for data free sources can’t give (tools.md) |
| Beginner vs expert output | Infer from the user’s wording. If unsure, give a plain summary with an expert appendix |
The market of each language
The market is the country the searchers are in. ccTLD, currency and addresses show where the business is, which is the same country only when its customers are local.
- The store, once a run has decided it.
site.json→markets[locale]with bothglandgl_basis(local,gsc,sells-abroadorowner). Aglwith nogl_basisis not a decision:ingest keyword-ideascopies it from the first file it sees (keyword_ideas.pydefaults to--gl us), and older stores took it from the business address.research_store.py statuslists those locales asundecided_markets: decide them with steps 2–3, then writeglandgl_basisintosite.json. - Customers are local (a shop, a clinic, a local service): the business’s country, for every language
(
gl_basis: local). Search Console isn’t asked: clicks from abroad on a translated page don’t move a local business’s market. - Customers come from abroad or buy from abroad (inbound travel, export, remote services):
- With Search Console: the country with the most clicks on that language version’s pages (Performance, Pages
filtered to the language’s path, then Countries)
[measured](gl_basis: gsc). - Without it
[inference](gl_basis: sells-abroad): a language that is a main language of the business’s own country keeps that country (Georgian for an operator in Georgia). Every other language gets the largest country it is written for (English for the same operator is not Georgia’s market).
- With Search Console: the country with the most clicks on that language version’s pages (Performance, Pages
filtered to the language’s path, then Countries)
- One country per locale. A second country that matters for the same language is a locale of its own with its
region (
en-gbbesideen-us); the store keeps them apart (research-store.md§3,ingest keyword-ideas). With Search Console it matters at about a fifth of that version’s clicks or more [heuristic]. Without it, add the second-largest country only for a language with several large ones (English, Spanish, Arabic). - Use it everywhere:
keyword_ideas.py --gl, located SERP samples, competitor checks and rank sets. hreflang codes are a separate decision (seo-international).
To change a market the store already holds, edit markets[locale].gl and gl_basis in site.json and research that
locale again. The store never drops data (research-store.md §5), so two things stay from the old country:
- SERPs: fetch the head terms again in the new country; a new snapshot replaces the old one.
- Keywords only the old country suggested: they keep their autocomplete evidence and keep scoring. They are the
ones whose
updated_atis still before the change (export keywords): mark those that don’t fit the new market withset-status --status skip.
Record the choice and its evidence under Assumptions.
What to ask (batched at the end of a run, each with a recommendation)
- Credentials and access the run could not get: API keys, OAuth sign-ins, Search Console/GA4/Bing access or exports, repo access, or a CDN allowlist when a bot challenge blocks the crawl. Finish everything possible first. Then list exactly what each missing access would add.
- Business decisions: markets or languages to enter, positioning, deleting or merging pages that earn traffic or revenue, spend above the budget, and the publishing mode while it is unset (below).
- Actions above the configured level (below). The run finishes everything up to that level, stops at the boundary and says which level or credential would finish it (for example, level 2: the pull request is open; level 3 merges it).
- The approval batch: each always-gated action the run prepared (below), one item each, with exactly what will happen and through which account. Claude carries out only the items the user approves in chat.
Never ask “should I continue?”, never ask to confirm a read-only step, and never stop a run to ask something that an assumption plus a note can cover.
The publishing mode, while it is unset
Each project chooses how its AI-written posts and their translations go live: automatic, once every automated gate
passes, or review, after a named human sign-off (references/research-store.md §2). The mode and the autonomy level
are separate. The level decides whether a run may merge or publish at all; the mode decides whether a person signs
off first.
While status shows publishing.mode as unset, a run behaves as review and never stops to ask. It adds one
question to the end-of-run batch, however many posts it held, with a recommendation:
- Recommend
reviewwhen a person already signs posts off (reviewed_byin existing posts), or the blog covers YMYL topics such as health, money, law or safety (references/google-rules.md§4), or EU public-interest topics (references/ai-content-review.md§1, row 27). - Otherwise recommend
automatic. Posts and translations then go live as soon as every gate inseo-blog-builder’s Checks passes, and a post that fails one still waits.
For example: “Publishing mode is unset, so this run held 4 posts for a human sign-off and shipped 8 translations
noindex. Recommendation: automatic, since nobody signs posts off today and the automated gates still hold back any
post that fails one. Answer automatic or review.”
When the user answers in chat, record it:
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/research_store.py" set-publishing --mode automatic. The next run follows it,
and the next step 1 of seo-blog-builder ships what was held back once its gates pass. A scheduled run writes the
question to seo-work/automation/inbox.md and never sets the mode: a scheduled prompt is not the user.
Autonomy levels (set once by the user)
| Level | Runs without asking | Still needs the user |
|---|---|---|
| 1. Analyse (default) | All reads: fetches, crawls, scripts, free APIs, paid calls within budget, reports, work log, research store updates (uncommitted) | Any change to the site or any external account |
| 2. Fix in code | Level 1 + edits to the site’s repo on a branch, commits, pushing that branch, opening a PR | Merging, deploying |
| 3. Ship | Level 2 + merging PRs that pass CI, submitting sitemaps / IndexNow for changed URLs | CMS publishing, account settings |
| 4. Operate | Level 3 + publishing to a CMS and Search Console sitemap submission through an authorised connector (claude_seo.py submit-sitemap, after the user’s one-time google-signin --sitemap-write; never the Indexing API) |
See “always gated” |
Never, at any level and not even with approval: buying reviews, buying links that pass ranking value (a paid
link is fine only with rel="sponsored" or nofollow, and the user buys it), and anything a vendor’s terms forbid
(skills/seo-automation §D). The run reports these as risks and prepares no action for them.
Always gated, at every level: messages to real people (outreach emails, DMs, review requests), disavow files, deleting content that earns traffic, Change of Address, CDN/WAF and DNS changes, Google Business Profile and Merchant Center edits, and spend above budget.
- The run prepares each one as an exact action (the recipients and message text, the values, or the file) and lists it in the end-of-run approval batch (What to ask, item 4).
- Once the user approves specific items in chat, Claude carries them out through the connected account and logs what
was done: the user’s email for messages, the Business Profile API for profile edits, the CDN’s or registrar’s
account for its rules, and Search Console in the user’s signed-in browser session for a disavow file or Change of
Address, which have no API. Without a connector, the run asks for that credential. Spend above budget is the
exception: the credit guard allows it only once the user raises the budget or
ask_above_pct(below). - Approval is per item and per run, and never carries over. A message to a real person is never sent without it.
- A scheduled run lists these items in
seo-work/automation/inbox.mdand carries out none of them: a scheduled prompt is not the user.
Credentials
- The user enters keys and sign-ins into Claude Code’s own configuration: plugin options,
claude mcp addheaders, OAuth pages, or environment variables. Never type a password into a website, and never paste keys into chat, logs or files that get committed. The research store is versioned with the repo, so it never holds secrets, raw paid responses or personal data (references/research-store.md§1). - Prefer API/MCP access over driving a vendor’s web UI. It is cheaper, more reliable, and allowed by the vendor’s terms.
- If a sign-in page, CAPTCHA or bot challenge appears, stop that step, record it as an access request, and continue with everything else.
Where the level and budgets live
The user sets them once with /plugin configure seo-suite@seo-suite:
| Key | Default | Meaning |
|---|---|---|
autonomy_level |
1 | The level above (1–4) |
audit_profile |
auto | The audit profile when the request names none: auto (the free stack, or the paid list for the site’s size when its budget is above 0), quick, full-free or paid (references/profiles.md) |
semrush_monthly_units, ahrefs_monthly_units |
0 | Monthly budget for paid calls, in API units. 0 denies paid calls |
dataforseo_monthly_usd, other_monthly_usd |
0 | Monthly budget for paid calls, in US dollars. 0 denies paid calls |
per_run_cap_pct |
25 | The most one run may spend, as a percentage of each monthly budget |
ask_above_pct |
5 | A single call estimated above this percentage of its monthly budget is deferred |
max_rows |
50 | Row limit on every paid call |
cache_ttl_days |
0 (per data type) | How long cached paid results are reused |
psi_api_key |
unset | Optional PageSpeed Insights key |
The credit guard (hooks/hooks.json) enforces the budget and limit keys on every paid call; audit_profile is read
by scripts/audit_profile.py, not the guard. A call above ask_above_pct is deferred: the guard denies it and records
it as a batched request in its requests.jsonl, and Claude raises it with the end-of-run questions. So “paid calls
within budget” means within the monthly budget, the per-run cap and the per-call threshold. If nothing is configured,
that is level 1 with budgets of 0: free tools only, which still covers a full audit. Setup and the guard’s details:
skills/seo-automation and references/paid-tools-automation.md §8.