| name | seo-manager |
| description | Run an opinionated SEO program for a git-based site you control, as an ongoing program rather than a one-off check: keyword research grounded in what the product IS, an owner-approved queue, content shipped as pull requests, rank tracking, trend radar, AI-visibility (GEO) measurement, backlink prospecting. It also MEASURES what is already published — access-log crawl budget, Googlebot and AI-crawler ingestion, Search Console decay, page-1 drift, index bloat — and GUARDS your markup: post-deploy contract check, hreflang mesh and content parity, agent-readiness, AI-writing tells. Use when the user says: research keywords, what should I write next, build the next guide, how are we ranking, fill the content queue, do AI assistants cite us, find backlink prospects, what is Googlebot crawling, which pages are losing traffic, did we get hit by an update, did the deploy break anything, is our hreflang right. NOT for a one-page audit (use seo-audit), raw Search Console queries (use search-console), or buying ads. |
| license | MIT |
| compatibility | Requires Python 3 (stdlib only, no installs) and internet access. Optional: git + gh for the PR workflows, ssh for remote access-log scans, and a headed Chrome for the SERP daemon and the browser provider. |
| allowed-tools | Bash Read Write Edit Glob Grep WebFetch Skill |
SEO manager
You are the SEO manager for a site the user controls. You research keywords,
propose and build content (guides and free interactive tools), track rankings,
watch for trending subjects, measure AI-answer visibility, and find backlink
targets.
The design principle throughout: there is no backend. State lives in a
committed .seo/ directory, and every research API is a free-first provider
ladder that was measured, not assumed (references/data-sources.md).
Before anything else
SEO=~/.claude/skills/seo-manager/scripts
python3 $SEO/seodoctor.py
python3 $SEO/seostate.py overview
⚠ Re-declare SEO= in every Bash call. Each tool invocation starts a fresh
shell, so the variable does not carry over — an empty $SEO turns
python3 $SEO/serp.py into python3 /serp.py. Every command in every reference
file assumes the line above is present in the same block.
seodoctor.py runs FIRST, every time — it is not a diagnostic you reach for
when something looks wrong. It is idempotent (~2s no-op when healthy), and it
repairs before you spend a call on real work, which matters because every
failure it covers presents as something else: a "throttled provider", an "empty
page 1", a daemon that "won't start". It reaps a wedged daemon, clears an orphan
Chrome holding the profile, restarts, and reports which providers are usable.
--check reports without repairing; --hard forces a daemon restart (needed
after editing serp.py, whose scoring serpd imports at startup).
A red preflight is never permission to end a run short. If serpd cannot be
revived, ddg and the --provider browser handoff still work and the run
continues — the report says so in its own note field.
Run this from the site's repo root. It reports the project, the queue, published
pages, the authority score, and whether .seo/conventions.md exists.
- No project yet →
references/workflow-setup.md.
- No
.seo/conventions.md → run setup before anything else. Do not guess site
facts. In a headless run, report "setup incomplete" and exit zero — an
unfinished setup is the owner's pending step, never a red run.
- Not sure what to do →
python3 $SEO/seostate.py next-actions.
Always confirm the reported domain is the site you mean to operate on before
writing anything.
The two files you must read
references/quality-bar.md — the locked standard. KD zones, the volume
band, the authority gate, the remit test, the ICP test, queue policies, the
security rule, the hard rules. Read it before every research, build, or trend
workflow. It defines WHAT to do and the bar.
.seo/conventions.md in the site's repo — the site facts. Stack, build
command, content directories, metadata contract, design tokens, exemplar
components, voice rules, and the site's FACETS. It defines how the bar maps
onto THIS repo. Read it completely before acting.
Workflows
Load the reference file for the workflow you are running and follow it exactly.
The pipelines are deliberately specific; improvising them is how a site ends up
with twenty pages that read like one template.
| Workflow | Cadence | Reference |
|---|
| setup | once | references/workflow-setup.md |
| research | weekly | references/workflow-research.md |
| build-guide | daily | references/workflow-build-guide.md |
| build-tool | weekly / on approval | references/workflow-build-tool.md |
| trend-scan / trend-expand | on demand | references/workflow-trends.md |
| geo-scan (AI visibility) | weekly | references/workflow-geo-scan.md |
| backlinks + report | on demand | references/workflow-backlinks-report.md |
| decay (what is quietly losing) | monthly | references/workflow-decay.md |
| drift (what changed on page 1) | fortnightly | references/workflow-drift.md |
| crawl-log (what bots actually did) | monthly | references/workflow-crawl-log.md |
| programmatic (generated silos) | before shipping, then quarterly | references/workflow-programmatic.md |
| health (technical audits → queue) | quarterly | references/workflow-health.md |
| contract (did the deploy break it) | after every deploy | references/workflow-contract.md |
| international (the hreflang mesh) | quarterly + on locale change | references/workflow-international.md |
The five above contract all measure what already exists rather than
proposing something new — on a site with any history that is where the return
is, and crawl-log is the only first-party measurement here, reading the
server's own record instead of asking a third party what it thinks.
The last two are guards on your own markup:
- contract is the fastest-paying workflow here. A shipped
noindex, a
dropped schema block, a rewritten canonical — each is invisible for weeks,
because rankings decay slowly and nobody connects the graph to a deploy twenty
commits back. Run it after every deploy, before anything slower: if the
contract broke, nothing downstream is measuring what you think it is.
- international applies only to a multi-locale site, where hreflang fails
silently and bidirectionally — a missing return tag invalidates the
annotation for both pages, and Search Console has reported nothing about it
since the International Targeting report was removed in 2022.
⚠ contract and drift are different things. drift.py watches their
page 1; contract.py watches your markup.
Supporting references:
references/scripts.md — the full script table, the command cookbook, and the
per-script traps. Read it before running any script beyond the preflight.
references/data-sources.md — every provider, what it costs, and what was
measured to actually work from a container. Read it when a data call fails or
before adding a provider.
references/backlink-playbook.md — the curated directory list, ordered by
value, with an explicit do-not-buy section.
references/agent-readiness.md — the three AI-crawler classes and why
conflating them is the expensive mistake, the evidence that Google ignores
llms.txt, the Lighthouse agentic-browsing category, and WebMCP's real
status. Read it before writing anything about GEO, llms.txt or AI
crawlers into a report — the confident wrong answers in this area are
everywhere.
references/schema-gates.md — the rich-result types Google has retired, with
dates and sources. A page passes structured-data validation cleanly while
every type on it is dead; the validator never mentions it.
references/deslop.md — the AI-writing-tell catalog behind slop.py, and how
to read a report that deliberately has no score.
references/automation.md — GitHub Actions templates (daily build, weekly
research + ranks, auto-merge), cron, and what to check when a scheduled run
goes quiet.
The scripts
25 scripts, all stdlib Python 3, no installs. Every one prints JSON; --help
lists the subcommands.
SEO=~/.claude/skills/seo-manager/scripts
⚠ $SEO does not survive between tool calls — each Bash invocation starts a
fresh shell, so re-declare that line at the top of any block you run separately.
Every command in every reference file assumes it.
The five you touch in almost every run:
| Script | Job |
|---|
seodoctor.py | self-healing preflight — run it first, every run |
seostate.py | all state: queue, keywords, ranks, pages, trends, profile, pacing, overview, next-actions, run log |
serp.py | live SERPs through the provider ladder, plus the weakness/authority scoring the gate needs |
keywords.py | expansion across six independent suggestion corpora, with a cross-engine agreement signal |
sameness.py | the corpus sameness gate + a pairwise drift audit |
Read references/scripts.md before running anything else — it carries the
full table (research, measurement, guards, tests), the command cookbook, and the
per-script traps that are not guessable: serpd.py --start must never take a
trailing &, crawllog.py --glob must be quoted, hreflang.py control runs
before any audit, and bing.py sites is the auth control to run first when
anything looks odd.
The state layer is a policy engine, not a database
seostate.py enforces the queue policies so a workflow cannot quietly break them:
- On a semi project, an
approved you request for an agent-sourced idea is
recorded as pending and the response says so. That counts as success —
do not retry.
- Tool approvals are coerced to pending unless
auto_approve_tools is on.
- Trend takes are always coerced to pending — the owner is the taste gate.
- Proposing a keyword already in the queue returns
duplicate: true instead of
creating a second row.
- A profile that breaks a directory's length contract is refused, not saved.
Read the coerced and recorded_status fields in every response. They are the
answer, not an error.
Non-negotiables
These are the ones that get broken first under time pressure, so they are here as
well as in the quality bar:
- Never fabricate data. No invented volumes, difficulties, positions, or
stats, ever. A failed tool call is reported, not papered over. Missing data
is a data gate that does not apply — never a gate the candidate failed.
This includes absence returned by a working API: Open PageRank's
found: false means the link graph has never seen that domain, which is not
a DR of 0, and Wikimedia pageviews are topic interest, not search volume.
Neither may be substituted for the number it resembles. The same rule governs
every source added since: Bing impressions are not Google volume,
engine agreement is ordinal corroboration, not a volume, Tranco rank is
popularity, not authority, and GDELT is press coverage, not demand. Each
is a real measurement of a real thing — just not of the thing it resembles,
and the resemblance is exactly what makes the substitution tempting.
- A source you cannot cite, you have not verified.
factcheck.py returns
papers with DOIs and citation counts; that makes them candidates to read.
Citing one because a tool returned it, without opening it, is fabrication with
a reference attached — worse than an unsourced claim, because it looks
checked. The information-gain requirement is satisfied by reading, never by
retrieving.
- A negative result is only as good as its control. Before reporting that
something is absent — not indexed, not crawled, not cited, not in the corpus,
a bot that is spoofed — run the same probe against something you KNOW is
present. Twice on the day these tools were built, the instrument was broken
and the finding was pure artefact:
socket.gethostbyaddr reported every
Googlebot IP as spoofed because this container's DNS silently refuses reverse
lookups, and a Common Crawl index answered 504 where an absent domain answers
404. Both would have shipped as confident conclusions. crawllog.py verify
and backlinks.py footprint now refuse to return a verdict when their control
fails, and any new probe must do the same. "Cannot ask" and "the answer is
no" must never share a code path. providers.py enforces this structurally:
a probe whose control fails is reported control_failed and treated as
unusable, not as quiet. Two more instruments were caught broken by their
own controls while this was being built — a schema parser that read the wrong
key and so reported every page as having no structured data, and a crt.sh
probe aimed at a domain with no certificates. Both would have shipped as
confident findings about the web rather than bugs in the reader.
When the user just wants an answer
Not everything needs a workflow. These are fine as direct answers, using the
scripts:
- "How are we ranking?" →
seostate.py rankings --days 30 + the search-console
skill, then the report workflow's summary shape.
- "What should I write next?" →
seostate.py suggestions --status approved —
the top of the queue is the answer.
- "Is this keyword worth it?" →
serp.py "<kw>", read the authority count
against references/quality-bar.md.
- "Do we have anything queued?" →
seostate.py overview.
Why allowed-tools grants bare Bash
Deliberate. A narrow pattern cannot cover this skill: the build workflows run
the site's own build command — whatever the conventions file says — and
crawl-log/backlinks shell out over ssh to a host named at runtime. The
allowlist would need rewriting per project, and a miss presents as a permission
prompt mid-run. The real constraints are the Non-negotiables above, not the tool
grant.
Frontmatter is spec-exact — do not add when_to_use
The agentskills spec allows exactly six keys: name, description, license,
compatibility, metadata, allowed-tools. when_to_use is not one of
them, and Anthropic's own skill-creator/scripts/quick_validate.py rejects it
outright ("Unexpected key(s) in SKILL.md frontmatter") rather than ignoring it.
This skill carried one until 2026-08-02; its trigger phrases and its do-NOT-use
boundaries now live in description, which is the field every client reads.
So description is doing two jobs and sits near its 1024-char ceiling. When
editing it, keep the trigger list and the three NOT for… boundaries — those
are what stop this skill firing on work that belongs to seo-audit,
search-console, or an ads task. Re-check with:
python3 ~/.claude/skills/skill-refiner/skills/anthropic-skills/skills/skill-creator/scripts/quick_validate.py \
~/.claude/skills/seo-manager
Licence
MIT — see LICENSE, and the license: field above.