Guardrails

Adding Slash Commands for Common WordPress Tasks

⏱ 14 min

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.

What you'll learn in this lesson
The two valid formats
Legacy .claude/commands/*.md files, and the current recommended .claude/skills/<name>/SKILL.md format.
Passing arguments into a command
The $ARGUMENTS, $0/$1, and named-argument substitution patterns.
Restricting a command to manual invocation only
disable-model-invocation, so Claude doesn't run a scaffolding task on its own judgment.
Building a real "scaffold a custom post type" command
End to end, from file to a tested /scaffold-cpt invocation.
Prerequisites

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.

Quote multi-word arguments

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

← Progressive Disclosure: Giving an Agent Just Enough Context Safe Full-Site Generation: From Plain-English Description to a Deployed Block Theme →