Use when fixing the debris an audit found in a Shopify catalog: bulk-archive dead/discontinued/zero-inventory products, clean up catalog debris, kill ghost review stars (empty star ratings a theme still renders after a review app was uninstalled), remove orphaned metafields left by an uninstalled app, strip HTML artifacts from product descriptions (double-nested bold, fully-bold paragraphs, empty tags), or fix messed-up / mangled product descriptions in bulk. Mutating cleanup work: archive vs...
Scanned 8/31/2026
Install to Claude Code
npx -y skills add kgelster/awesome-ecom-skills --skill shopify-catalog-cleanup --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Shopify Catalog Cleanup?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/kgelster-shopify-catalog-cleanup)More formats (shields.io, HTML) on the badges page.
---
name: shopify-catalog-cleanup
description: >-
Use when fixing the debris an audit found in a Shopify catalog: bulk-archive
dead/discontinued/zero-inventory products, clean up catalog debris, kill ghost
review stars (empty star ratings a theme still renders after a review app was
uninstalled), remove orphaned metafields left by an uninstalled app, strip HTML
artifacts from product descriptions (double-nested bold, fully-bold paragraphs,
empty tags), or fix messed-up / mangled product descriptions in bulk. Mutating
cleanup work: archive vs delete, metafieldsDelete, description rewrites. Not for
FINDING what's wrong (use shopify-catalog-audit) or filling missing metadata
like SEO titles and alt text (use shopify-seo-metadata).
compatibility: >-
Requires Shopify Admin API access (custom-app token or Shopify CLI 3.x).
GraphQL written against Admin API 2025-07. Minimum scopes: read_products,
write_products (products carry metafield read/write).
---
# Shopify Catalog Cleanup
Fixing the debris an audit found. This is the mutating counterpart to
`shopify-catalog-audit` (which finds problems) and `shopify-seo-metadata` (which
fills missing metadata). Everything here writes to the catalog, so the whole
skill is built around one rule: **preview the count, then mutate.**
## Store access
**Lane A: custom-app token (scriptable).** In Shopify admin: Settings → Apps
and sales channels → Develop apps → create an app → grant `read_products` and
`write_products`, install, copy the Admin API access token. Export it; never
write it to a committed file:
```bash
export SHOPIFY_STORE="your-store.myshopify.com"
export SHOPIFY_ACCESS_TOKEN="<your Admin API access token>" # env, not disk
```
```bash
curl -s "https://$SHOPIFY_STORE/admin/api/2025-07/graphql.json" \
-H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"{ shop { name } }"}'
```
**Lane B: Shopify CLI OAuth (no stored token).** `shopify store auth --store
$SHOPIFY_STORE --scopes read_products,write_products` then `shopify store
execute`. Good for token-less stores where the owner logs in interactively.
**Toolkit preflight.** Lane B rides on the Shopify CLI: run `shopify version`
first and install it if missing. For the full Admin GraphQL schema and
validated execution, pair this skill with Shopify's official AI toolkit
plugin. In Claude Code, check `claude plugin list`; if it isn't there:
```bash
claude plugin marketplace add Shopify/Shopify-AI-Toolkit
claude plugin install shopify-plugin@shopify-ai-toolkit
```
Recommended, not required: Lane A needs only curl and a token. The toolkit
gives your agent the API; this skill gives it the playbook.
## The one rule: preview count, then mutate
Every recipe in [references/recipes.md](references/recipes.md) pairs a read-only
count/preview query with the mutation it feeds. The discipline is not optional:
1. **Run the count first.** `products(query:"…")` with a filter returns
`count` and a page of titles. Read them.
2. **Sanity-check the number against expectation.** If you expected ~40 dead
products and the count says 900, **stop and re-scope**: your filter is
wrong, not the store. A count far above expectation is a bug in your query.
3. **Only then mutate,** and prefer the reversible path (below).
## Non-destructive doctrine
- **Archive, never delete.** Archiving a product (`status: ARCHIVED`) hides it
from the storefront and channels but keeps the record, its handle, its history,
and its URL-redirect potential. It is fully reversible: flip status back to
`ACTIVE` or `DRAFT`. Deletion is not reversible: the product, its variants,
and its metafields are gone. Default to archive; only delete when the owner
explicitly asks and understands it is permanent.
- **Tag what you touch.** When you bulk-archive, add a dated tag
(`archived-2026-07-05`) in the same mutation. That tag is your undo list: to
reverse, query `tag:archived-2026-07-05` and set status back. Without it you
cannot cleanly separate your batch from products that were already archived.
- **Read before you write.** A read query is cheap; an unscoped mutation is not
reversible. Verify a write landed by re-reading the object through the Admin
API, not the storefront (the storefront is CDN-cached and lies about
freshness).
- Never print or commit the Admin API access token.
## Recipes, not scripts
The GraphQL blocks in the reference are copy-and-adapt starting points: your
agent writes throwaway code per engagement against the current schema, runs it,
discards it. Bulk mutations should batch and respect throttling: read the
`throttleStatus` in the `extensions` block and back off when
`currentlyAvailable` drops, rather than hammering a fixed rate.
## Three fix classes
### 1. Bulk-archiving dead products
Design the criteria before you touch anything. Common "dead" signals, combined
with AND (a product is rarely dead on one signal alone):
- Zero total inventory across all variants, and
- Zero sales in a trailing window (e.g. 12 months), and
- Created/updated long enough ago to not be a new launch, and
- Not in a collection you care about, or already tagged discontinued.
Turn that into a `products(query: …)` search filter, run the **count** query,
read the sample titles, confirm the number is in the ballpark you expected, then
run `productUpdate` to set `status: ARCHIVED` and append the dated tag. The
unarchive path is the same query on your tag, flipping status back. Recipes:
[references/recipes.md](references/recipes.md).
### 2. Orphaned metafield debris (the ghost-star class)
Classic symptom: a store uninstalled a product-review app (Shopify's own Product
Reviews is the usual culprit), but the theme still renders **empty star ratings**
on every product. The app left its metafields behind: the `spr.reviews` field the
Product Reviews app wrote, plus a `reviews` namespace holding `reviews.rating` and
`reviews.rating_count`. The theme's star snippet reads them, finds `0`, and paints
five empty stars.
Fix safely:
1. **Confirm the app is truly gone.** Check Settings → Apps; the namespace must
belong to an app that is uninstalled, not merely paused. Deleting metafields
an installed app owns will just have the app rewrite them.
2. **Inventory the orphaned namespaces.** Query the product metafield
definitions and a sample of products for every suspect namespace (`spr` and
`reviews` for the Product Reviews app) so you know exactly which
`namespace.key` pairs you are removing and how many products carry them.
3. **Delete via `metafieldsDelete`**, keyed by owner GID + namespace + key,
covering both `spr.reviews` and the `reviews.rating` / `reviews.rating_count`
fields. Preview the affected-count first, same as archiving; batch the
identifiers to stay under rate/cost limits.
4. **Re-read** a few products to confirm the fields are gone, then reload the
storefront product: the empty stars should be gone (the theme snippet may
also need its now-dead reference removed; flag that to whoever owns the theme).
**Matrixify warning:** a blank cell in a Matrixify update **deletes** that
metafield, it does not skip it. That is a footgun for accidental deletion and a
tool for deliberate bulk removal: either way, know which you are doing. For
spreadsheet-driven bulk work and that trap, see `shopify-matrixify`.
### 3. HTML artifacts in descriptions
Editors, app injections, and copy-paste leave residue in `descriptionHtml`:
double-nested `<strong><strong>…`, whole paragraphs wrapped in bold, empty
`<p></p>` / `<span></span>` tags, stray inline styles. Detect cheaply with an
index scan (pull `descriptionHtml` for the scope, regex for the artifact
patterns) rather than eyeballing products one by one. Fix by rewriting
`descriptionHtml` per product via `productUpdate`: a targeted string transform,
not a from-scratch rewrite that would lose real formatting.
**Log old and new for every rewrite.** As you go, write each rewrite (product
id, old `descriptionHtml`, new `descriptionHtml`, status) to a CSV, the same
discipline `shopify-seo-metadata` mandates for meta writes. That log is two
things: your diff review (read a sample of old→new pairs before declaring
done) and your only undo list. Archiving gets a dated tag to query for
reversal; a rewrite has nothing, so without the log a bad transform is
unrecoverable. This matters downstream too: skills that generate from body
HTML (SEO meta, for one) inherit whatever a bad rewrite left behind.
**Post-edit scan doctrine.** After ANY bulk content edit, sweep the affected
scope again for residue: double-bold, orphaned tags, half-applied
transforms, **before** you declare done. A bulk regex fix routinely leaves a
new artifact on the 3% of products whose markup didn't match your assumption.
The passing re-scan is the evidence; "the transform ran" is not.
## References
- [references/recipes.md](references/recipes.md): count-before-mutate archive
query + mutation, metafield-namespace inventory + `metafieldsDelete`,
description-artifact scan + safe rewrite.
## Provenance and maintenance
Last verified: 2026-07-05. GraphQL pinned to Admin API 2025-07; Shopify
deprecates versions on a rolling quarterly schedule, so verify against
[shopify.dev](https://shopify.dev/docs/api/admin-graphql) before trusting a
version-specific claim. Read-only re-verification a stranger can run (a
metafield-namespace inventory and an archived-product count, both non-mutating):
```bash
# how many products are already archived, and a metafield namespace sample
curl -s "https://$SHOPIFY_STORE/admin/api/2025-07/graphql.json" \
-H "X-Shopify-Access-Token: $SHOPIFY_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"{ productsCount(query:\"status:archived\") { count } products(first:5) { nodes { title metafields(first:20) { nodes { namespace key } } } } }"}'
```
Canonical docs: [productUpdate](https://shopify.dev/docs/api/admin-graphql/latest/mutations/productupdate),
[metafieldsDelete](https://shopify.dev/docs/api/admin-graphql/latest/mutations/metafieldsdelete).
These skills capture operational lessons the docs don't; they are not a
replacement for the reference.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!