Skip to content
SEO
Hook

Hooks

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

From plugin
codex-seo
81027 skills2 agents1 hook
Install
$ npx -y skills add agricidaniel/codex-seo --agent claude-code

Ships with codex-seo. Installing the plugin gets these hooks.

What fires, and when

PostToolUse

  • MatchesEdit|Writepython3 "${CODEX_PLUGIN_ROOT}/hooks/validate-schema.py" "$FILE_PATH"
Read hooks/hooks.json

Where it lives

  • hooks/pre-commit-seo-check.shGitHub
    Read the script
    #!/usr/bin/env bash
    set -euo pipefail
    
    # Pre-commit SEO validation hook for Codex.
    #
    # Hook configuration in ~/.codex/settings.json:
    # {
    #   "hooks": {
    #     "PreToolUse": [
    #       {
    #         "matcher": "Bash",
    #         "hooks": [
    #           {
    #             "type": "command",
    #             "command": "~/.codex/skills/seo/hooks/pre-commit-seo-check.sh",
    #             "exitCodes": { "2": "block" }
    #           }
    #         ]
    #       }
    #     ]
    #   }
    # }
    #
    # NOTE: The matcher is "Bash" (tool name only). This script runs on ALL
    # Bash tool uses. It checks if there are staged files before proceeding.
    # If there are no staged changes, it exits 0 immediately.
    
    ERRORS=0
    WARNINGS=0
    
    # Check if there are staged changes โ€” exit early if not
    if ! git diff --cached --quiet 2>/dev/null; then
        : # There are staged changes, proceed with checks
    else
        exit 0  # No staged changes, nothing to check
    fi
    
    echo "๐Ÿ” Running pre-commit SEO checks..."
    
    # Check staged HTML-like files
    STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(html|htm|php|jsx|tsx|vue|svelte)$' || true)
    
    if [ -z "${STAGED_FILES}" ]; then
        echo "โœ“ No HTML files staged โ€” skipping SEO checks"
        exit 0
    fi
    
    for file in ${STAGED_FILES}; do
        if [ ! -f "${file}" ]; then
            continue
        fi
    
        # Check for placeholder text in schema
        if grep -qiE '\[(Business Name|City|State|Phone|Address|Your|INSERT|REPLACE)\]' "${file}" 2>/dev/null; then
            echo "๐Ÿ›‘ ${file}: Contains placeholder text in schema markup"
            ERRORS=$((ERRORS + 1))
        fi
    
        # Check title tag length
        TITLE=$(grep -oP '(?<=<title>).*?(?=</title>)' "${file}" 2>/dev/null | head -1 || true)
        if [ -n "${TITLE}" ]; then
            TITLE_LEN=${#TITLE}
            if [ "${TITLE_LEN}" -lt 30 ] || [ "${TITLE_LEN}" -gt 60 ]; then
                echo "โš ๏ธ  ${file}: Title tag length ${TITLE_LEN} chars (recommend 30-60)"
                WARNINGS=$((WARNINGS + 1))
            fi
        fi
    
        # Check for images without alt text
        if grep -qP '<img(?![^>]*alt=)' "${file}" 2>/dev/null; then
            echo "โš ๏ธ  ${file}: Images found without alt text"
            WARNINGS=$((WARNINGS + 1))
        fi
    
        # Check for deprecated schema types
        if grep -qE '"@type"\s*:\s*"(HowTo|SpecialAnnouncement)"' "${file}" 2>/dev/null; then
            echo "๐Ÿ›‘ ${file}: Contains deprecated schema type"
            ERRORS=$((ERRORS + 1))
        fi
    
        # Check for FID references (should be INP)
        if grep -qi 'First Input Delay\|"FID"' "${file}" 2>/dev/null; then
            echo "โš ๏ธ  ${file}: References FID โ€” should use INP (Interaction to Next Paint)"
            WARNINGS=$((WARNINGS + 1))
        fi
    
        # Check meta description length
        META_DESC=$(grep -oP '(?<=<meta name="description" content=").*?(?=")' "${file}" 2>/dev/null | head -1 || true)
        if [ -n "${META_DESC}" ]; then
            META_LEN=${#META_DESC}
            if [ "${META_LEN}" -lt 120 ] || [ "${META_LEN}" -gt 160 ]; then
                echo "โš ๏ธ  ${file}: Meta description length ${META_LEN} chars (recommend 120-160)"
                WARNINGS=$((WARNINGS + 1))
            fi
        fi
    done
    
    echo ""
    if [ "${ERRORS}" -gt 0 ]; then
        echo "๐Ÿ›‘ ${ERRORS} critical error(s) found โ€” commit blocked"
        echo "Fix the errors above and try again."
        exit 2
    elif [ "${WARNINGS}" -gt 0 ]; then
        echo "โš ๏ธ  ${WARNINGS} warning(s) found โ€” commit allowed"
        exit 0
    else
        echo "โœ“ All SEO checks passed"
        exit 0
    fi
    
  • hooks/validate-schema.pyRunsGitHub
    Read the script
    #!/usr/bin/env python3
    """Post-edit schema validation hook for Codex.
    
    Validates JSON-LD schema after file edits. Returns exit code 2 to block
    if critical validation errors found.
    
    Hook configuration in ~/.codex/settings.json:
    {
      "hooks": {
        "PostToolUse": [
          {
            "matcher": "Edit|Write",
            "hooks": [
              {
                "type": "command",
                "command": "python3 ~/.codex/skills/seo/hooks/validate-schema.py \"$FILE_PATH\"",
                "exitCodes": { "2": "block" }
              }
            ]
          }
        ]
      }
    }
    
    Note: matcher filters by tool name only (Edit, Write). The script itself
    checks if the file contains schema markup before validating.
    """
    
    import json
    import re
    import sys
    import os
    from typing import List
    
    
    def validate_jsonld(content: str) -> List[str]:
        """Validate JSON-LD blocks in HTML content."""
        errors = []
        pattern = r'<script\s+type=["\']application/ld\+json["\']\s*>(.*?)</script>'
        blocks = re.findall(pattern, content, re.DOTALL | re.IGNORECASE)
    
        if not blocks:
            return []  # No schema found โ€” not an error
    
        for i, block in enumerate(blocks, 1):
            block = block.strip()
            try:
                data = json.loads(block)
            except json.JSONDecodeError as e:
                errors.append(f"Block {i}: Invalid JSON โ€” {e}")
                continue
    
            if isinstance(data, list):
                for item in data:
                    errors.extend(_validate_schema_object(item, i))
            elif isinstance(data, dict):
                errors.extend(_validate_schema_object(data, i))
    
        return errors
    
    
    def _validate_schema_object(obj: dict, block_num: int) -> List[str]:
        """Validate a single schema object."""
        errors = []
        prefix = f"Block {block_num}"
    
        # Check @context
        if "@context" not in obj:
            errors.append(f"{prefix}: Missing @context")
        elif obj["@context"] not in ("https://schema.org", "http://schema.org"):
            errors.append(f"{prefix}: @context should be 'https://schema.org'")
    
        # Check @type
        if "@type" not in obj:
            errors.append(f"{prefix}: Missing @type")
    
        # Check for placeholder text
        placeholders = [
            "[Business Name]",
            "[City]",
            "[State]",
            "[Phone]",
            "[Address]",
            "[Your",
            "[INSERT",
            "REPLACE",
            "[URL]",
            "[Email]",
        ]
        text = json.dumps(obj)
        for p in placeholders:
            if p.lower() in text.lower():
                errors.append(f"{prefix}: Contains placeholder text: {p}")
    
        # Check for deprecated types
        schema_type = obj.get("@type", "")
        deprecated = {
            "HowTo": "deprecated September 2023",
            "SpecialAnnouncement": "deprecated July 31, 2025",
            "CourseInfo": "retired June 2025",
            "EstimatedSalary": "retired June 2025",
            "LearningVideo": "retired June 2025",
            "ClaimReview": "retired June 2025 โ€” fact-check rich results discontinued",
            "VehicleListing": "retired June 2025 โ€” vehicle listing structured data discontinued",
        }
        if schema_type in deprecated:
            errors.append(f"{prefix}: @type '{schema_type}' is {deprecated[schema_type]}")
    
        # Check for restricted types used incorrectly
        restricted = {"FAQPage": "restricted to government and healthcare sites only (Aug 2023)"}
        if schema_type in restricted:
            errors.append(f"{prefix}: @type '{schema_type}' is {restricted[schema_type]} โ€” verify site qualifies")
    
        return errors
    
    
    def main():
        if len(sys.argv) < 2:
            sys.exit(0)
    
        filepath = sys.argv[1]
    
        if not os.path.isfile(filepath):
            sys.exit(0)
    
        # Only validate HTML-like files
        valid_extensions = (".html", ".htm", ".jsx", ".tsx", ".vue", ".svelte", ".php", ".ejs")
        if not filepath.endswith(valid_extensions):
            sys.exit(0)
    
        try:
            with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read()
        except (OSError, IOError):
            sys.exit(0)
    
        errors = validate_jsonld(content)
    
        if not errors:
            sys.exit(0)
    
        # Categorize errors
        critical_keywords = ["placeholder", "deprecated", "retired"]
        critical = [e for e in errors if any(kw in e.lower() for kw in critical_keywords)]
        warnings = [e for e in errors if e not in critical]
    
        if warnings:
            print("โš ๏ธ  Schema validation warnings:")
            for w in warnings:
                print(f"  - {w}")
    
        if critical:
            print("๐Ÿ›‘ Schema validation ERRORS (blocking):")
            for e in critical:
                print(f"  - {e}")
            sys.exit(2)  # Block the edit
    
        sys.exit(1)  # Warnings only โ€” proceed
    
    
    if __name__ == "__main__":
        main()
    

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.

Ships withcodex-seo

Codex-first SEO analysis suite with 1 orchestrator skill, 26 specialist workflows, 24 TOML agent profiles, MCP/API extensions, deterministic headless runners, and premium audit report generation.

Get the whole plugin