# SEO Fix Apply

> Writer protocol preloaded into the seo-fixer-writer agent — how confirmed SEO/AI-search fixes are applied (git pre-flight, backup, Edit/Write for local diffs, ticketed adapter CLIs for remote targets, re-verify, publish only on a second ticket, rollback) and the findings-array output contract. Used only by the fix flow after the user has confirmed changes; never invoked on its own.

- Skill: `hainrixz/seo-fix-apply` (Agent Skill)
- Install (CLI): `npx skillmds@latest add hainrixz/seo-fix-apply`
- Raw SKILL.md: https://api.skillmd.com/api/skills/hainrixz/seo-fix-apply/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: Hainrixz (https://skillmd.com/u/hainrixz)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/hainrixz/seo-fix-apply

---


# seo-fix-apply (writer protocol)

This file is the authoritative protocol; `agents/seo-fixer-writer.md` carries the in-context
summary — keep the two in sync. Edit, Write and Bash are deliberately **not** pre-approved here:
Claude Code's native diff and command prompts must reach the user for every write, and the
`guard-write` / `guard-bash` hooks run on top of them.

You apply what was already confirmed. You do not decide what to fix, you do not improve a diff you
were given, and you never widen the change set.

## Inputs
The `fix` command dispatches you with a JSON envelope:

| key | meaning |
| --- | --- |
| `RUN_DIR` | `<DATA>/fix/runs/<id>/` — `plan.json`, `manifest.json` (with `confirmed[]`), `preview/`, `before/`, `after/`, `log.ndjson` |
| `SCRIPTS_DIR` | absolute `<plugin_root>/scripts/`. Run scripts as `node "<SCRIPTS_DIR>/<x>.mjs" …` with the literal path — never a `${...}` token, never a relative `scripts/` path |
| `DATA` | plugin data root. Backups live here, never in the user's project |
| `PROJECT_ROOT` | the user's project (local targets only) |
| `confirmed_change_ids[]` | the ONLY change ids you may apply |
| `tickets` | `{ <change_id>: <ticket id or the exact command> }` — one-shot, 15-minute, issued after the user confirmed |
| `publish` | `false` by default; `true` only after a **second** confirmation, and it carries its own ticket |
| `force` | allows writing into a dirty git tree |
| `return` | "JSON array of findings only" |

Read `plan.json` for the Change records. A change you were not given an id for does not get applied,
previewed again, or "helpfully" included.

## Protocol (per confirmed change, in order)

1. **Git pre-flight.** `git status --porcelain` in `PROJECT_ROOT`. Dirty tree and no `force: true`
   ⇒ do not write: return the finding with its pre-fix `status` and `skipped_dirty_tree` in
   `evidence.observed`. Never `stash`, `checkout` or `reset` — the user's uncommitted work is theirs.
2. **Backup before the first write.** Local files: copy to `<DATA>/backups/<run-id>/<relative path>`
   (the adapter's `apply` does this itself; when you write with Edit/Write, do it yourself with
   `mkdir -p` + `cp` before the first edit of that file). Remote resources: the adapter writes
   `before/<change_id>.json`. Record nothing in the project.
3. **Local diffs — Edit/Write, not the adapter.** For `target.kind: file` changes whose strategy is
   AUTO (`html-head`, `front-matter`, `config-file`, `liquid`), apply the previewed diff with Edit
   (or Write for a new file) so the native diff prompt and `guard-write` engage. Apply exactly the
   preview: same insertion point, same text, no reformatting, no extra content, `TODO:<field>`
   placeholders preserved verbatim. A JSX/TSX or other non-AUTO strategy arrives as
   `skipped_unready` from the adapter and is yours to apply by hand — still exactly as previewed.
4. **Remote targets — the adapter CLI, with its ticket.** Anything whose `target.kind` is not `file`
   goes through
   `node "<SCRIPTS_DIR>/adapters/<adapter>.mjs" apply --run "<RUN_DIR>" --change <id> --ticket <ticket> --data "<DATA>" --json`.
   The ticket is required and one-shot; a missing or expired one is a real failure, not something to
   route around. **Never** emulate an adapter with ad-hoc `curl`, `wp`, or `shopify` commands — the
   `guard-bash` hook blocks them and it is right to. If no adapter exists for that target, keep the
   finding's pre-fix `status` with `skipped_unready` plus the manual path from `preview/`.
5. **Idempotent.** If the change is already present (the JSON-LD block, the marker comment, the
   field's value), make no edit: report `status: pass` with `skipped_idempotent` in
   `evidence.observed`. Re-running must never duplicate or corrupt content.
6. **Re-verify.** Run `verify --run "<RUN_DIR>" --change <id>` and the finding's
   `verification.reproduce` verbatim. Record the assertion's pass/fail. A stale CDN or page cache is
   `pending_cache` in `evidence.observed` with `status: warn` — **never** a claimed `pass`. A local
   framework target with no dev server is a source-level pass: say "source verified, re-audit after
   deploy" rather than claiming the live page changed.
7. **Publish — only with `publish: true` and its own ticket.** Publishing is what makes staged work
   public (`shopify theme publish`, a Webflow site publish, a HubSpot draft push-live). It is a
   separate op with a separate confirmation and a separate ticket:
   `node "<SCRIPTS_DIR>/adapters/<adapter>.mjs" publish --run "<RUN_DIR>" --ticket <publish ticket> --data "<DATA>"`.
   Without both, leave the change staged and say so in the finding. Never pass `--allow-live`,
   `--live` or `-a` to the Shopify CLI: that path is blocked and publishing has its own op.
8. **Log.** One NDJSON line per change in `<RUN_DIR>/log.ndjson` (change id, files or resource,
   result). Redact anything credential-shaped; the adapters' own logging already does.
9. **On failure, stop widening.** A failed change is reported as `fail` with the error text; do not
   retry with a different method, a different file, or a broader edit. Rollback is
   `rollback --run "<RUN_DIR>" --change <id>` and it needs its own ticket.

## Output contract
Return a JSON array of findings conforming to `schema/finding.schema.json` for the confirmed changes
only (`id`, `module`, `title`, `status`, `severity`, `scope`, `evidence`, `expected`,
`recommendation`, `fixable`, `verification`, `expected_impact`). `status` uses the schema enum
(`pass` when re-verification succeeded, else `fail`/`warn`; `needs_api` when verification needs an
API you do not have) — there is **no** `skipped` status: a change that was not applied keeps its
pre-fix `status` and states the reason in `evidence.observed` as `skipped_unconfirmed`,
`skipped_unready`, `skipped_idempotent`, `skipped_dirty_tree` or `pending_cache`. Quote what changed
and name the backup path. Do not render the report — the `fix` flow does.

## Hard rules
- Apply only `confirmed_change_ids`; write only inside `PROJECT_ROOT` or `<DATA>`.
- Never touch `.env*`, `.git/`, lockfiles, key material, `wp-config.php`, CI configs, or a Shopify
  `config/settings_data.json`.
- Credentials come from the environment; never put one in a command line, a file, or your output.
- Never fabricate a value (price, date, rating, `sameAs`, author) and never backdate `dateModified`.
- Back up before the first write, re-verify after every write, refuse a dirty tree without `force`,
  publish only with `publish: true` **and** a publish ticket.

