Imported from webdados/wp-template-rebase (
SKILL.md). Install upstream withnpx skills add webdados/wp-template-rebase. Copyright stays with the author.
WordPress template rebase
Refreshing an overridden template means reconstructing what somebody customized in the first place, which is the part nobody writes down. This skill makes that mechanical: the customizations are recovered by diffing the override against the pristine template of the release it was copied from, replayed onto the current upstream template by a real three-way merge, and left annotated so the next pass is cheap. A rebase is a diff plus a merge plus a verification pass, not a reading of the new template and a hopeful rewrite.
Hard rules
- Both sides always come from the project's public repository at the matching tag. Never diff
against the copy installed on the site. That copy can have been hand-edited, and the base side
is an older release that is not installed at all, so mixing sources makes the two ends
inconsistent. The site is read only for version numbers. See
references/sources.md. - Never invent a base version. Either the override's
@versiongives it exactly, or it is inferred, and an inferred base downgrades the run to "ask before writing". - Never write a file that fails any of the three verification checks in Phase 5. A rebase that cannot be verified is a rewrite, and rewriting is what this skill exists to avoid.
- Never bump
@versionto the project version. It becomes the@versionof the fetched target template, which is usually an older number. Confusing the two is the most common mistake in this job, so read "Two version numbers" inreferences/sources.mdbefore touching a header. - Read the code around the template, not just the template. That is the theme, or the plugin, that holds the override. A customization's purpose usually lives outside the file, in the filter that drives it. Reporting the objective without checking that code produces a description of the syntax, which is useless to the person reviewing it.
Inputs
Any of these:
- A theme or child theme, or a subfolder of one such as
woocommerce/. - A plugin that carries overrides of another project's templates, served through a filter such
as
woocommerce_locate_template. They usually sit in the plugin's owntemplates/folder with nowoocommerce/prefix, sotemplates/emails/email-addresses.phpoverrides WooCommerce'stemplates/emails/email-addresses.php. Point at the plugin root and onlytemplates/and any registry folder in it are scanned, or point at a subfolder to scan exactly that. - A standalone file outside any install, such as a copy someone sent over. It needs
--as, the path it would have inside a theme, and either--wp-rootor--installedto say which release to compare against. With--installedno site is needed at all.
Invoked with no target, ask for one. Infer one only when the session has already been working
in a single unambiguous set of files and no second candidate is plausible. A site merely mentioned
earlier in the conversation is not that, and neither is a file that happened to be open, whatever is
sitting in references/cache/, or the theme scanned last. This skill edits real templates on real
sites, so a wrong guess means editing production somewhere nobody asked about: when in doubt, ask
and stop there. A bare theme or site name is not a target either, resolve it to a path before
Phase 0.
Scripts
All are read-only, take no credentials, and cache into references/cache/ which is gitignored.
# Inventory a theme, a plugin, a folder or one file: what is overridden, from which project, how stale.
scripts/scan-overrides.sh "/path/to/htdocs/wp-content/themes/mytheme"
scripts/scan-overrides.sh "/path/to/htdocs/wp-content/plugins/myplugin"
scripts/scan-overrides.sh --as woocommerce/emails/email-addresses.php --installed 11.1.2 ~/file.php
scripts/scan-overrides.sh --as woocommerce/emails/email-addresses.php --wp-root /path/to/htdocs ~/file.php
scripts/scan-overrides.sh --offline "/path/..." # no network, upstream column reads "?"
# Fetch one pristine file at one release. Prints the cached path. Exit 2 means no such file there.
scripts/fetch-upstream.sh --kind plugin --slug woocommerce --version 9.8.0 \
--path templates/emails/email-addresses.php
scripts/fetch-upstream.sh --kind theme --slug storefront --version 4.6.2 --path single.php
scripts/fetch-upstream.sh --kind core --version 6.8 --path wp-includes/theme-compat/header.php
scripts/fetch-upstream.sh --kind github --repo owner/name --version 1.2.3 --path templates/x.php
# Releases available for a wordpress.org project, oldest first. Used by base inference.
scripts/list-versions.sh --kind theme --slug storefront --max 4.6.2 --sample 10
# Run every Phase 5 check on a rebased file. Exit 3 means at least one failed.
scripts/verify-rebase.sh --base b.php --ours o.php --target t.php --merged m.php
Workflow
Phase 0: resolve the targets
Run scripts/scan-overrides.sh on the target and show its table before doing anything else. It
works out whether the target sits in a theme, a plugin or neither, locates the WordPress root, reads
the theme, parent theme or plugin headers, classifies each override, and reports staleness against
the real upstream release.
Classification order, which the script applies and which matters when a path is ambiguous. For a
plugin, a leading templates/ is stripped first, since that folder is the plugin's own template
root. For a standalone file, the path given with --as is used.
- A first segment in
references/projects.tsv, such aswoocommerce/, means that project, at its registered template root. - In a theme, the same relative path existing in the parent theme means the parent theme.
- In a plugin, another installed plugin shipping
templates/<path>means that plugin. If more than one does, the first is assumed and the report says so: check which one the plugin's template loader actually overrides. - A first segment matching an installed plugin slug means that plugin, at
templates/<rest>. - A match under
wp-includes/theme-compat/means WordPress core. - Otherwise it is a custom template, not an override.
A file that fetches as 404 at the installed version is not an override. It is either custom, or
upstream removed or renamed it. Both cases are reported and skipped, never merged. A real example:
emails/supplier-processing-order.php, in a Storefront child theme and later in a site plugin, is a
custom WooCommerce email paired with its own WC_Email subclass, and only looks like an override
because of where it sits.
Then let the user pick what to process, unless they already named a single file.
Phase 1: fetch base and target
- base =
fetch-upstream.shat the tag equal to the override's@version. Sanity check: the fetched file's own@versionmust equal the override's. If it does not, the header lies or was hand-edited, so fall through to inference. - target =
fetch-upstream.shat the tag equal to the installed project version. - No
@versionin the override: infer the base by the procedure inreferences/sources.md, coarse sweep withlist-versions.sh --sample, deduplicate by hash, keep the revision with the smallest diff, then refine around it. Record that the base was inferred: it changes the decision in Phase 5.
Phase 2: isolate and understand the customizations
diff -u "$BASE" "$OURS" # the customization set
diff -u "$BASE" "$TARGET" # what upstream actually changed, hunk by hunk
For each hunk of the first diff:
- Classify it: addition, replacement, removal, or pure whitespace and reformatting. Whitespace-only hunks are noise, drop them rather than carrying them forward, and say so in the report.
- Reuse any existing marker text or docblock note as the stated intent.
- Grep the theme or plugin holding the override for every custom hook name it introduces. A
filter such as
mytheme_show_item_pricewill have its counterpart infunctions.php,includes/or the plugin's modules. For a standalone file there is nothing to grep, so ask where its hooks are consumed. That code is what the customization is actually for. It also catches a dead customization, one whose hook nothing filters any more, which is worth reporting rather than carrying forward silently.
Phase 3: mechanical merge
git merge-file -p --diff3 "$OURS" "$BASE" "$TARGET" > "$MERGED"
git merge-file works outside a repository, so no scratch repo is needed. Clean hunks land
deterministically, and that is the whole point: no reasoning step gets the opportunity to quietly
drop an upstream change. Only the conflicts need judgement.
Phase 4: resolve, adapt, annotate
Resolve each conflict by keeping upstream's structure and re-expressing the customization inside it, guided by the intent from Phase 2 rather than by the old line shapes. When upstream has rewritten a region, a customization that was three lines may become one line somewhere else. That is an adaptation, and the report has to say so.
Then annotate every customization in the file, including the ones the skill did not introduce.
Marker format. Default to PHP comments, because HTML comments are emitted into the rendered page or into the email source where recipients can read them. Use HTML comments only where the file already uses that style consistently and the block is pure markup.
<?php // BEGIN mytheme: billing address cell behind a filter, hidden on this site ?>
<?php if ( apply_filters( 'mytheme_show_billing_address', true ) ) : ?>
...
<?php endif; ?>
<?php // END mytheme ?>
Where a customization replaced or removed core code, keep that code commented out inside the block, labelled with the version it came from. Pure additions get markers but no commented core code, because there is nothing they replaced.
<?php
// BEGIN mytheme: last name removed, the checkout hides it too
/* Core 11.0.0 had:
<p class="woocommerce-form-row form-row-last">
<label for="account_last_name"><?php esc_html_e( 'Last name', 'woocommerce' ); ?></label>
<input type="text" class="input-text" name="account_last_name" id="account_last_name" />
</p>
*/
?>
<?php // END mytheme ?>
The marker owner is derived from the site being worked on, never assumed. mytheme above is a
placeholder, not a default. Work down this list and stop at the first that yields a name:
- An attribution already present in that file, or in the other overrides beside it, in whatever
form it takes: a trailing
// Acme, an<!-- added by Acme -->, a docblock note. Matching what is already there keeps a file consistent with its neighbours. - The
Authorheader of the theme'sstyle.css, or of the plugin's main file. - The theme or plugin name, or failing that its text domain.
Never carry a name over from another project, another site, or an example in this skill. The marker says who owns the customization on this site, so a wrong one credits somebody else's work, and on a site handed between agencies that is actively misleading. If none of the three yields anything usable, ask rather than guess.
Comment out core code with /* */, never with //. Template code contains ?>, and a ?>
inside a one-line comment still closes PHP mode, so everything after it leaks to the browser or the
email as literal text. Inside a block comment ?> is inert. Verified 2026-08-28 against PHP 8.
The only thing to watch in a block comment is a literal */ in the code being commented out.
Finally, update the docblock:
- Bump
@versionto the target template's@version. - Maintain a
Customizations:list summarising each marked block in one line, so the next pass can read the intent before diffing anything.
* @version 10.6.0
*
* Customizations:
* - Billing address cell wrapped in mytheme_show_billing_address.
* - Shipping cell also prints the billing phone as a fallback, and the billing email.
Phase 5: verify, then decide
Four checks. All must pass before writing without asking. Run the script, do not improvise greps: an unescaped regex character in an ad-hoc grep reports a missing upstream change that is actually present, and sends the whole pass chasing a phantom.
scripts/verify-rebase.sh --base "$BASE" --ours "$OURS" --target "$TARGET" --merged "$MERGED"
- Lint:
php -lon the merged file. - Upstream coverage: every non-blank line of the target appears in the merged file, ignoring indentation, since customizations reindent the code they wrap. A missing upstream line is the failure this skill exists to prevent, so an unexplained one blocks the write.
- Customization coverage: every line ours added over base appears in the merged file. Lines listed here are not automatically failures, a deliberately adapted or re-marked line shows up too, but every one of them must be explained in the report.
- Stale variables: a variable referenced in the merged file that is never assigned there and
does not appear anywhere in the target. This is the failure mode nothing else catches: upstream
deletes a variable, a customization still passes it to a hook, the merge reports no conflict and
php -lpasses, and the template raises an undefined variable notice at runtime with an empty value reaching whatever the hook renders. The check reads PHP's own token stream rather than the raw text, so a variable named in a comment is not mistaken for a reference.
Closing invariant, the strongest signal available: diff "$TARGET" "$MERGED" must contain
nothing but the annotated customization blocks, and their count must equal the number of
customizations found in Phase 2. If it contains anything else, something was invented or lost.
State the count in the report either way.
Confidence gate
Write when the base was resolved exactly from the version header, every customization has a clear objective, no conflict was resolved ambiguously, and all three checks pass.
Ask first when any of these hold:
- the base was inferred rather than taken from a matching
@version, - a customization's objective could not be established from the template and the code around it,
- upstream restructured a region so a customization had to be adapted rather than carried,
- a customization looks dead, its hook no longer used anywhere,
- markers are being retrofitted onto a change whose purpose had to be guessed.
Do not rebase, and say why when upstream rewrote the template beyond recognition, the template was removed or renamed upstream, or the customization set cannot be expressed against the new structure. Recommend the alternative instead: drop the override in favour of the hooks the new template exposes, or restart the override from the current upstream file and reapply the customizations deliberately. An override that cannot be rebased honestly is worse than no override, because it silently freezes the site on old markup.
Before the first write of a run, ask how to handle the original: overwrite, overwrite and keep a backup copy, or stage the result and hand over the path. Keep that answer for the rest of the run.
A .bak left inside the theme is usually readable over HTTP, since the server will not run a
file that no longer ends in .php and serves its source as plain text instead. That exposes the
template, and any comment in it, to anyone who guesses the name. Say so when that option is chosen,
and remind the user to delete it once they are satisfied with the result.
After writing, read the file back and compare it against the staged result. A write over SFTP or a network mount can truncate or fail silently, and a half-written template is worse than a stale one.
Report
Follow references/report-template.md. Per template: the version table, what upstream brought in,
one block per customization giving what it is, what it is for including the supporting theme or plugin code,
what upstream did to that region, and how it was carried across, then the verification results and
the decision.
The customization blocks are the part worth writing carefully. Everything else is bookkeeping.
Cost notes
- Every fetch is cached under
references/cache/<kind>/<slug>/<version>/<path>, so a second run on the same theme is nearly free. The cache is gitignored and safe to delete. - Base inference is the expensive path, roughly 10 to 15 fetches per file. Prefer a template with an
@versionheader, and where a project has a GitHub repository use the commits-for-path API call instead of sweeping releases. scan-overrides.shmakes one fetch per file. Use--offlinefor a quick structural look.