Some WordPress tasks come up over and over across a plugin’s lifetime: scaffold a new custom post type, register a new REST route, add another ability. Typing out the same paragraph of instructions each time invites drift, this time you forget to mention the capability check, last time you forgot the text domain. A slash command fixes the wording once and reuses it every time, with room for the one or two things that actually change per use.
Claude Code connected from Lesson 2, and the wp-secure-code skill from Lesson 4 already in the repo, since the commands you build here should produce code that skill’s rules already govern.
Step 1: know which format you’re targeting
Claude Code originally supported custom commands as plain markdown files in
.claude/commands/, one file per command, the filename becoming the command name.
That format still works today. The current recommended format wraps the same idea in
a skill: a directory at .claude/skills/<name>/SKILL.md, which supports
everything the legacy format does plus supporting files and finer-grained invocation
control. New commands in this lesson use the skill format, since it’s what
Anthropic’s own documentation points you toward now.
Step 2: scaffold a custom post type on demand
# File: terminal, from your plugin's repo root
mkdir -p .claude/skills/scaffold-cpt
---
# File: .claude/skills/scaffold-cpt/SKILL.md
name: scaffold-cpt
description: Scaffold a new custom post type registration for this plugin.
argument-hint: [post-type-slug] [singular-label] [plural-label]
disable-model-invocation: true
---
Register a new custom post type using `register_post_type()`, following this
plugin's existing conventions:
1. Post type slug: $0
2. Singular label: $1
3. Plural label: $2
Requirements:
- Hook the registration on `init`, matching how other post types in this
plugin are already registered (check includes/post-types/ for the pattern
to follow).
- Set `'public' => true`, `'show_in_rest' => true`, and a `'capability_type'`
appropriate for this post type, don't default to the built-in `post`
capabilities unless the post type is genuinely meant to share them.
- Include `'labels'` generated from the singular and plural labels above,
using the standard set WordPress core's own post type labels use.
- Add the new file at includes/post-types/class-{slug}-post-type.php,
matching the naming and class structure of existing files in that
directory.
- Before writing any hook or function call you're not fully certain exists,
verify it against the WordPress core files in this project.
Do not register the post type yet if a post type with this slug already
exists elsewhere in the plugin, check first and report a conflict instead.
disable-model-invocation: true matters here: scaffolding a new post type is a
deliberate action you trigger, never something you want Claude deciding to run on its
own initiative mid-conversation because your description sounded relevant.
Step 3: test it
/scaffold-cpt event Event Events
$0 becomes event, $1 becomes Event, $2 becomes Events. Confirm the
generated file follows your existing naming convention, includes show_in_rest, and
picked a sensible capability type rather than defaulting to post’s.
If a label has a space in it, quote it: /scaffold-cpt event "Event" "Events" is
unambiguous, /scaffold-cpt event Event Events also works here since single-word
labels don’t need it, but a slug like staff member would need
/scaffold-cpt "staff-member" "Staff Member" "Staff Members" to avoid the space
splitting into extra arguments.
Step 4: a second command for adding a new ability
If you’ve worked through Course 2 in this track, registering a new ability follows a predictable shape too, namespace, label, description, schemas, permission callback, execute callback. That repetition is exactly what a command is for:
---
# File: .claude/skills/add-ability/SKILL.md
name: add-ability
description: Register a new WordPress ability for this plugin.
argument-hint: [ability-name] [short description]
disable-model-invocation: true
---
Register a new ability named "my-plugin/$0" inside the `wp_abilities_api_init`
action, following this plugin's existing ability registrations as a pattern
to match.
The ability should: $ARGUMENTS
Requirements:
- Write a `permission_callback` that checks a specific capability, never
return true unconditionally.
- Write `input_schema` and `output_schema` as proper JSON Schema, matching
the level of detail already used by other abilities in this plugin.
- Set `meta.mcp.public` explicitly, default to false unless I've told you
this ability should be reachable by external AI clients.
- Add the registration in the same file or location as this plugin's other
ability registrations, don't create a new pattern.
/add-ability get-order-status "returns the current status of a WooCommerce order by order ID"
Here, $0 pulls just the ability’s short name for the namespaced identifier, while
$ARGUMENTS (the full argument string) carries the free-form description into the
body of the instructions, both substitutions doing different jobs in the same
command.
Recap
A slash command turns a repeatable WordPress task, a new post type, a new ability,
into a one-line invocation with the boilerplate and the easy-to-forget requirements
baked in once. Use the current .claude/skills/<name>/SKILL.md format, set
disable-model-invocation: true on anything you want to trigger deliberately rather
than have Claude decide to run, and use $0/$1/$N for structured arguments or
$ARGUMENTS for a single free-form one. Commit these to the repo, and every session
working on this plugin gets the same reliable scaffolding.
Resources & further reading
- Extend Claude with skills, code.claude.com
- Commands reference, code.claude.com
- register_post_type() function reference, developer.wordpress.org