wp-cli/scaffold-command
WP-CLI scaffold command generates starter code for WordPress post types, taxonomies, blocks, plugins, and child themes. Includes commands for building themes from Underscores with options for activation, SASS, and WooCommerce support.
First, a scope note. Despite the Laravel framing of this assessment, wp-cli/scaffold-command is not a Composer library and has nothing to do with artisan. It is a WP-CLI package — a set of CLI commands that write WordPress boilerplate files to disk. Think of it as php artisan make:* fused with laravel new, but for WordPress themes, plugins, custom post types (CPTs), and taxonomies. There is no service provider, no config/ file, no runtime dependency to add to composer.json. You use it exactly the way you would use artisan from a terminal: one-shot, file-generating, and best wrapped in a Makefile target or a Docker entrypoint.
Minimal setup:
wp --info
wp scaffold --help # lists every subcommand
wp help scaffold plugin # full option reference for one command
wp package install wp-cli/scaffold-command
wp package list # verify which version wins
Note that the installed copy shadows the bundled copy; wp package uninstall wp-cli/scaffold-command reverts to the bundled version.wp bootstraps wp-load.php to resolve wp-content paths), or pass --path= / --url= explicitly. Almost every useful subcommand needs the WP install to be bootstrappable, because that's how it decides where files land.Where to look first:
wp scaffold --help and wp help scaffold <sub>, which are always in sync with the installed version. This is more trustworthy than the README.templates/ directory in the source repo if you need to see precisely what will be generated before running it.First use case to try. Add a CPT to a plugin you already own, printing to the terminal so you can read it before anything touches disk:
cd /var/www/site
wp scaffold post-type book --label="Books" --textdomain=acme-core --plugin=acme-core --raw
--raw emits only the register_post_type() call. Drop the flag and you get a full file with the init hook wiring. If the output looks right, rerun with the raw form redirected to a scratch file, review it in your editor, and commit it yourself. That output-review-then-commit loop is the whole day-to-day value of this package.
Pattern 1 — CPTs and taxonomies as files inside your own plugin. This is the highest-value use, because hand-writing the labels array is pure tedium.
wp scaffold post-type movie --label=Movie --textdomain=acme-core --plugin=acme-core
wp scaffold taxonomy genre --post_types=movie --textdomain=acme-core --plugin=acme-core
Files land at wp-content/plugins/acme-core/post-types/movie.php (and taxonomies/genre.php). The scaffold does not wire them up — you must require_once them from the main plugin file yourself, typically inside a small foreach ( glob( __DIR__ . '/post-types/*.php' ) as $file ) { require_once $file; }. Follow it with wp rewrite flush (the registry changed, so permalinks are stale) or you will stare at 404s.
Pattern 2 — Output-to-STDOUT for diffs. For anything you're inserting into an existing registrar class rather than a fresh file, use --raw without --theme/--plugin and paste, or redirect to a temp file and git diff it. Never let the scaffolder write into a file you've hand-tuned — see Gotchas on --force.
Pattern 3 — Theme-scoped scaffolding for classic themes. --theme with no value targets the active theme; --theme=simple-life targets a specific one. That's two meanings on one flag and it's easy to misfire. Use the explicit slug form in scripts.
Pattern 4 — Plugin bootstrap with CI baked in. For a greenfield plugin:
wp scaffold plugin acme-core \
--plugin_name="Acme Core" \
--plugin_author="Acme" \
--ci=github \
--activate
Then, later, retrofit tests on a plugin that already exists (including one not created by this tool):
wp scaffold plugin-tests acme-core --ci=github
The same applies to themes via wp scaffold theme-tests. This split (plugin vs plugin-tests) is the useful one: scaffold minimal, add test infrastructure when the plugin proves it's worth keeping.
Pattern 5 — Monorepo / non-standard paths. --dir=<path> places the plugin at <path>/<slug> instead of wp-content/plugins/<slug>. This is the flag you want when the plugin lives in a Composer-managed packages/ tree or a Bedrock-style layout. For plugin-tests, --dir is also how you point at a plugin whose directory name diverges from its slug.
Pattern 6 — Multisite and activation in automation. --activate activates; for networks you need --activate-network (plugins) or --enable-network (themes), and you almost always need --url= to pick the right site context. Scripts that run against a remote alias work too: wp scaffold plugin acme-core --activate @staging.
Pattern 7 — Reproducible local environments. Put the scaffold calls in a bin/setup.sh or a wp-env afterStart hook so every teammate's site has the same CPTs and the same plugin skeleton. Combine with --force only for files nobody edits (the main plugin file you regenerate deliberately), and never for files under active development.
Pattern 8 — Blocks: don't. wp scaffold block is deprecated and the README says so. For any new block use npx @wordpress/create-block. The only remaining reason to reach for wp scaffold block --plugin=<slug> is adding a legacy PHP-registered block to a plugin that already exists and can't take @wordpress/scripts yet.
The README leads with a deprecation you should believe. "Scaffolding plugins and themes with wp scaffold is no longer recommended for modern WordPress development." The generated plugin ships a Grunt-based build (Gruntfile.js, grunt-wp-i18n, wp_readme_to_markdown) and a CircleCI config by default. If you're starting fresh in 2025+, take the CPT/taxonomy subcommands (genuinely useful, no maintenance burden) and skip the plugin/theme boilerplate in favor of @wordpress/create-block and a hand-rolled Composer setup.
--force is silent and total. There is no prompt and no backup. Always git status clean before regenerating, and prefer generating to STDOUT for anything other than a brand-new file.
Nothing is auto-loaded. The generated post-types/*.php and taxonomies/*.php files are inert until you require them. Forgetting this is the single most common "the scaffold is broken" report.
Rewrite rules are not flushed. Registration alone doesn't create the pretty permalinks. wp rewrite flush --hard after deploying a new CPT/taxonomy to any environment, including production.
--theme is a dual-purpose flag. Bare --theme = active theme; --theme=slug = that theme. Same shape for --plugin=slug. In scripts always pass the value.
Respect WordPress's own name rules — the scaffolder doesn't check. Post type keys must be ≤ 20 characters and must not collide with reserved names (post, page, attachment, revision, nav_menu_item, …). Taxonomy keys must be ≤ 32 characters. Exceed those and registration silently fails at runtime with a _doing_it_wrong notice, not at scaffold time.
--textdomain defaults to the theme/plugin context, not to something you chose. If your i18n setup uses a different domain than the slug, pass --textdomain explicitly on every invocation. Mixed domains in generated labels are annoying to unpick later.
Path resolution requires a bootstrappable WordPress. wp scaffold post-type --plugin=foo from the wrong directory fails with a confusing "couldn't find" error rather than "no WP install here." Set --path= or cd to the WP root first. In Docker, confirm the CLI container actually has wp-content mounted writable.
Test scaffolding is the sharp edge. bin/install-wp-tests.sh needs: a MySQL/MariaDB client binary (mysql/mariadb), Subversion (it checks out the WP test suite from develop.svn.wordpress.org), curl or wget, and DB credentials with CREATE/DROP privileges. On macOS, brew install subversion mysql-client. tests/bootstrap.php reads WP_TESTS_DIR, so export it in CI and in your local shell profile — otherwise PHPUnit dies with a bare "could not find" message. Do not commit a local phpunit.xml over phpunit.xml.dist.
Check what version you're actually running. A wp package install of the dev branch can shadow the bundled release and change generated output subtly between machines. Put wp package list in your onboarding docs.
No config-file overrides, no extension points. Unlike artisan stub:publish, you cannot point this package at your own templates. The templates are baked into the package. If your team has a house style (namespace, PHP 8.2 syntax, declare(strict_types=1), PSR-4 autoloading), your realistic options are (a) fork the package and wp package install your fork, or (b) treat the scaffold as a throwaway first draft and run phpcbf/Rector over the output. Option (b) ages better.
Success messages vs. stdout redirection. The README's own example pipes wp scaffold taxonomy … > taxonomy.php, implying notices don't pollute the redirect. Verify in your WP-CLI version before depending on it in a build script — safest is --raw plus an explicit output path, or generate into the plugin dir and let the tool place the file.
Child themes: --parent_theme is mandatory and unvalidated. wp scaffold child-theme acme --parent_theme=twentytwentyfive will happily create a child of a parent that isn't installed; --activate then fails. Confirm the parent slug exists (wp theme list) first.
wp scaffold _s makes a network call. Theme generation downloads from underscores.me, so air-gapped CI or a corporate proxy will fail here and nowhere else. Cache or pre-vendor the theme if that matters.
Forking is the real extension path. This is a WP-CLI package: it registers commands via WP_CLI::add_command() and lives in templates/. To customize output, fork, adjust templates/, run composer phpcs + composer behat (the project's own gates), and install your fork. If you're writing your own generator, wp scaffold package (a different package) is the recommended starting point rather than hand-rolling registration.
How can I help you explore Laravel packages today?