Skip to content
Marketing
Hook

Hooks

What claude-seo runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
claude-seo
28k33 skills20 agents1 hook

Claude Code

> /plugin marketplace add AgriciDaniel/claude-seo
> /plugin install claude-seo@agricidaniel-claude-seo

What fires, and when

PostToolUse

  • MatchesEdit|Writenode
Read hooks/hooks.json

Where it lives

  • hooks/run-python-hook.jsGitHub
    Read the script
    #!/usr/bin/env node
    "use strict";
    
    const { spawnSync } = require("child_process");
    const path = require("path");
    
    // Same list as valid_extensions in validate-schema.py. The hook fires on every
    // Edit and Write, so skip the Python probe for files that cannot hold JSON-LD.
    const SCHEMA_FILE_EXTENSIONS = [".html", ".htm", ".jsx", ".tsx", ".vue", ".svelte", ".php", ".ejs"];
    
    // The skip belongs to the schema hook only; any other hook script always runs.
    const SCHEMA_HOOK_SCRIPT = "validate-schema.py";
    
    function isSchemaHook(hookScript) {
      return path.basename(hookScript) === SCHEMA_HOOK_SCRIPT;
    }
    
    function cannotHoldSchema(filePath) {
      // An unsubstituted "${tool_input.file_path}" leaves the path to the stdin event.
      if (!filePath || filePath.includes("${")) {
        return false;
      }
      const lower = filePath.toLowerCase();
      return !SCHEMA_FILE_EXTENSIONS.some((ext) => lower.endsWith(ext));
    }
    
    function stripWrappingQuotes(value) {
      return value.replace(/^["']|["']$/g, "");
    }
    
    function pythonCandidates() {
      const candidates = [];
      if (process.env.CLAUDE_SEO_PYTHON) {
        candidates.push({
          label: "CLAUDE_SEO_PYTHON",
          exe: stripWrappingQuotes(process.env.CLAUDE_SEO_PYTHON),
          args: [],
        });
      }
      candidates.push(
        { label: "py -3", exe: "py", args: ["-3"] },
        { label: "python3", exe: "python3", args: [] },
        { label: "python", exe: "python", args: [] },
      );
      return candidates;
    }
    
    function isStoreStubOutput(text) {
      return /Microsoft Store|WindowsApps|App execution alias|was not found/i.test(text);
    }
    
    function probe(candidate) {
      const script = "import sys; print(sys.executable); print(sys.version.split()[0])";
      const result = spawnSync(candidate.exe, [...candidate.args, "-c", script], {
        encoding: "utf8",
      });
      const output = `${result.stdout || ""}\n${result.stderr || ""}`;
      return result.status === 0 && Boolean((result.stdout || "").trim()) && !isStoreStubOutput(output);
    }
    
    function main() {
      const [, , hookScript, ...hookArgs] = process.argv;
      if (!hookScript || (isSchemaHook(hookScript) && cannotHoldSchema(hookArgs[0]))) {
        process.exit(0);
      }
    
      for (const candidate of pythonCandidates()) {
        if (!probe(candidate)) {
          continue;
        }
        const result = spawnSync(candidate.exe, [...candidate.args, hookScript, ...hookArgs], {
          stdio: "inherit",
        });
        if (result.error) {
          continue;
        }
        process.exit(result.status === null ? 1 : result.status);
      }
    
      console.error(
        "Claude SEO hook could not find Python. Tried CLAUDE_SEO_PYTHON, py -3, python3, python.",
      );
      process.exit(1);
    }
    
    main();
    
  • hooks/validate-schema.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """Post-edit schema validation hook for Claude Code.
    
    Validates JSON-LD schema after file edits. The file is already written when
    a PostToolUse hook runs, so nothing here can undo the edit; the hook makes
    sure Claude reads what is wrong:
    
    - critical errors (placeholders, deprecated or retired types): exit 2, with
      the errors on stderr, which Claude Code feeds back to Claude;
    - warnings only: exit 0, with the warnings as hookSpecificOutput
      additionalContext JSON on stdout, which Claude reads without an error
      notice (exit 1 would show them to the user only).
    
    Hook configuration in ~/.claude/settings.json:
    {
      "hooks": {
        "PostToolUse": [
          {
            "matcher": "Edit|Write",
            "hooks": [
              {
                "type": "command",
                "command": "node",
                "args": [
                  "${CLAUDE_PLUGIN_ROOT}/hooks/run-python-hook.js",
                  "${CLAUDE_PLUGIN_ROOT}/hooks/validate-schema.py",
                  "${tool_input.file_path}"
                ]
              }
            ]
          }
        ]
      }
    }
    
    Note: matcher filters by tool name only (Edit, Write). run-python-hook.js
    exits before starting Python when the edited file's extension is not in
    valid_extensions below, and the script itself checks if the file contains
    schema markup before validating.
    """
    
    import json
    import os
    import re
    import sys
    from typing import Any, List
    
    BRACKET_PLACEHOLDERS = (
        "[Business Name]",
        "[City]",
        "[State]",
        "[Phone]",
        "[Address]",
        "[Your",
        "[INSERT",
        "[URL]",
        "[Email]",
    )
    BARE_PLACEHOLDER_RE = re.compile(r"\bREPLACE(?:_[A-Z]+)*\b")
    
    # Match every <script ...>...</script> pair, then filter on the type attribute.
    # The previous pattern required ``type`` to be the first and only attribute, so
    # blocks carrying a CSP ``nonce``, an ``id`` or ``data-*`` attributes, or an
    # unquoted type value were skipped without validation.
    #
    # The attribute group is a small tokenizer, not a plain ``[^>]*``: it consumes a
    # double-quoted value, a single-quoted value, or a run of characters that is
    # neither a quote nor ``>``. A ``[^>]*`` scan ends the tag at the first ``>`` it
    # sees, quoted or not, so an attribute value containing ``>`` (``data-cond="a>b"``,
    # a templated nonce) truncated the tag early and fed the remainder of the
    # attributes plus the real body to the JSON parser as garbage. Treating a quoted
    # span as atomic keeps an embedded ``>`` from ending the tag prematurely.
    # The fallback class excludes both quote characters: if it matched an
    # apostrophe, the alternation would be ambiguous and a tag with many
    # apostrophes and no closing tag would backtrack exponentially, hanging the
    # blocking hook.
    _ATTRS_RE = r'(?:"[^"]*"|\'[^\']*\'|[^"\'>])*'
    SCRIPT_TAG_RE = re.compile(
        r"<script\b(" + _ATTRS_RE + r")>(.*?)</script\s*>", re.DOTALL | re.IGNORECASE
    )
    LD_JSON_TYPE_RE = re.compile(
        r"""(?:^|\s)type\s*=\s*"""
        r"""(?:"application/ld\+json"|'application/ld\+json'|application/ld\+json(?=\s|$))""",
        re.IGNORECASE,
    )
    
    # Server- or client-side template expressions that render JSON-LD at runtime.
    # The hook runs on .jsx/.tsx/.vue/.svelte/.php/.ejs sources, where the script
    # body is frequently an expression rather than literal JSON. Those blocks cannot
    # be validated statically and must not be reported as invalid JSON.
    SERVER_TEMPLATE_RE = re.compile(
        r"""^(?:
            <\?(?:php\b|=)            # <?php ... ?> / <?= ... ?>
          | <%                        # EJS / ERB
        )""",
        re.VERBOSE,
    )
    COMPONENT_EXPRESSION_RE = re.compile(
        r"""^(?:
            \{\{                      # Vue / Handlebars / Twig
          | \{@html\b                 # Svelte
          | \$\{                      # JS template literal
          | \{\s*[A-Za-z_$][\w$.]*    # JSX expression: {schema} / {JSON.stringify(...)}
        )""",
        re.VERBOSE,
    )
    COMPONENT_EXTENSIONS = (".jsx", ".tsx", ".vue", ".svelte")
    
    SCHEMA_ORG_CONTEXTS = frozenset(
        {"https://schema.org", "http://schema.org", "https://schema.org/", "http://schema.org/"}
    )
    
    
    def _configure_utf8() -> None:
        """Keep hook diagnostics printable on legacy Windows console encodings."""
        for stream in (sys.stdout, sys.stderr):
            reconfigure = getattr(stream, "reconfigure", None)
            if reconfigure:
                reconfigure(encoding="utf-8", errors="replace")
    
    
    def _extract_ld_json_blocks(content: str) -> List[str]:
        """Return the bodies of every ``<script type="application/ld+json">`` block.
    
        Attribute order, extra attributes (``nonce``, ``id``, ``data-*``), tag case
        and unquoted type values are all accepted; only the type value is decisive.
        """
        blocks = []
        for attributes, body in SCRIPT_TAG_RE.findall(content):
            if LD_JSON_TYPE_RE.search(attributes):
                blocks.append(body)
        return blocks
    
    
    def _is_template_expression(block: str, filepath: str = "") -> bool:
        """True when the script body is rendered at runtime rather than literal JSON.
    
        Server-side markers (PHP, EJS) are never valid JSON and are skipped for
        every file type. Component expressions (JSX, Vue, Svelte, template
        literals) are only skipped in component sources, so a malformed object
        literal in a plain ``.html`` file is still reported.
        """
        if SERVER_TEMPLATE_RE.match(block):
            return True
        if filepath.lower().endswith(COMPONENT_EXTENSIONS):
            return bool(COMPONENT_EXPRESSION_RE.match(block))
        return False
    
    
    def _is_schema_org_context(value: Any) -> bool:
        """Accept the schema.org context in its string, list and object forms."""
        if isinstance(value, str):
            return value in SCHEMA_ORG_CONTEXTS
        if isinstance(value, list):
            return any(_is_schema_org_context(item) for item in value)
        if isinstance(value, dict):
            return _is_schema_org_context(value.get("@vocab"))
        return False
    
    
    def validate_jsonld(content: str, filepath: str = "") -> List[str]:
        """Validate JSON-LD blocks in HTML content."""
        errors = []
        blocks = _ex

Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.

@agricidaniel
Ships withclaude-seo

Universal SEO skill for Claude Code. 26 sub-skills + 19 sub-agents covering technical SEO, E-E-A-T, schema, GEO/AEO, agent readiness (Lighthouse Agentic Browsing, WebMCP, llms.txt), backlinks, local SEO, e-commerce, international SEO, Google APIs, and PDF/Excel reporting. 9 optional extensions for live SEO data.

28k33 skills20 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
27.8k
Star history of AgriciDaniel/claude-seo on GitHub, a chart by star-history.com

More plugins that ship hooks.

aaron-marketing

aaron-marketing

Gives a marketing team an AI staff for planning and running campaigns across SEO, content, paid ads, email, influencer work and product launches. Each area comes with ready-made commands that work on data you supply.

7 hooks+13.1k
@aaron-he-zhu@aaron-he-zhu
Install Plugin
digital-marketing-pro

digital-marketing-pro

Runs a consistent marketing strategy process across every brand in a portfolio, producing the same planning files in the same order so agencies and in-house teams can audit depth and catch compliance gaps. Covers SEO audits, ad copy, A/B test planning and AI search visibility.

164 skills+3942
@indranilbanerjee@indranilbanerjee
Install Plugin
ralph-wiggum-marketer

ralph-wiggum-marketer

Runs an autonomous loop for SaaS content marketing and copywriting, with commands to initialize, run, check status, and cancel it. A stop hook keeps the loop going when an agent stops.

1 skill+2780
@muratcankoylan@muratcankoylan
Install Plugin
claude-repurpose

claude-repurpose

Content Repurposing Engine for Claude Code, Turn 1 piece of content into 10+ platform-optimized posts in seconds.

21 skills+2167
@agricidaniel@agricidaniel
Install Plugin
content-ideas

content-ideas

Scrapes tracked competitor accounts on X, Instagram, TikTok and YouTube, scores what is performing well, and turns it into a daily feed of content ideas framed against your own brand.

1 skill+2147
@bradautomates@bradautomates
Install Plugin
email

email

Professional email management, deliverability optimization, and marketing strategy powered by Claude Code.

6 skills+2132
@agricidaniel@agricidaniel
Install Plugin
claude-seo-ai

claude-seo-ai

The SEO + AI-search (GEO/AEO) optimization toolkit for Claude Code, two-score audit + opt-in fixer. Built for 2026-2027.

34 skills+281
@hainrixz@hainrixz
Install Plugin
akii-seo-ai-search-optimizer

akii-seo-ai-search-opt…

A free Claude Code toolkit for SEO, AI answer engine optimization, and generative engine optimization. Its skills cover AI visibility checks, broken link audits, competitor scorecards, content briefs, and content strategy plans.

12 skills+378
@akii-technologies-ltd@akii-technologies-ltd
Install Plugin

Questions about the hooks in claude-seo.

Which coding agents can use the hooks in claude-seo?
The hooks in claude-seo install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.