Guardrails

Building a Guardrailed Claude Code Plugin/Skill

⏱ 16 min

Telling an agent “please escape your output” once, in one conversation, doesn’t survive a new session, a compacted context window, or a teammate who never saw that message. A skill does. This lesson builds a real Claude Code skill that loads WordPress security and coding-standards expectations automatically, so escaping, sanitization, capability checks, and nonces stop being something you have to remember to ask for.

What you'll learn in this lesson
What a Claude Code skill actually is
A SKILL.md file with YAML frontmatter, loaded automatically or invoked with /name.
Where to put a project-level skill
The .claude/skills/ directory, committed alongside your plugin code.
Writing security rules Claude actually follows
Concrete, checkable rules referencing real WordPress escaping and sanitization functions.
Referencing WordPress Coding Standards directly
Pointing the skill at the real WPCS ruleset instead of restating it from memory.
Confirming the skill actually changes generated output
A before/after test using the same prompt with the skill off and on.
Prerequisites

Claude Code connected from Lesson 2, and a plugin or theme repository on disk, ideally one living inside your local Studio site from Lesson 1 so you can immediately test what gets generated.

Step 1: understand the current skill format

Claude Code’s custom slash commands were originally plain markdown files in .claude/commands/. Those still work, but the current, recommended format is a skill: a directory containing a SKILL.md file with YAML frontmatter, placed in .claude/skills/<skill-name>/SKILL.md for a project, or ~/.claude/skills/ for something available across every project on your machine. A skill can be invoked directly with /skill-name, or loaded automatically by Claude when its description matches what you’re asking for.

Step 2: create the skill directory

# File: terminal, from your plugin's repo root
mkdir -p .claude/skills/wp-secure-code

Project-level, committed to the repo, means every teammate and every fresh Claude Code session working on this plugin gets the same guardrails without having to set anything up themselves.

Step 3: write the skill

---
# File: .claude/skills/wp-secure-code/SKILL.md
name: wp-secure-code
description: WordPress security and coding-standards rules to apply whenever writing or editing PHP in this plugin. Use when generating output that touches user input, database queries, capability checks, or anything rendered to the browser.
---

# WordPress secure coding rules for this plugin

Apply every rule below to any PHP you write or edit in this repository. These are not
suggestions, treat a violation as a bug in your own output before you show it to the
user.

## Escaping output

- Every value printed into HTML must be escaped at the point of output, not the
  point of storage. Use `esc_html()` for text content, `esc_attr()` for HTML
  attributes, `esc_url()` for URLs, and `wp_kses_post()` only when the value is
  expected to contain a limited set of safe HTML.
- Never print a variable directly into a template with a bare `echo $value;` unless
  it has already passed through one of the functions above on that same line.

## Sanitizing input

- Every value read from `$_POST`, `$_GET`, `$_REQUEST`, or `$_COOKIE` must be passed
  through an appropriate `sanitize_*()` function before use: `sanitize_text_field()`,
  `sanitize_email()`, `sanitize_key()`, or `absint()` for expected integers.
- Never trust a value's shape just because JavaScript validated it client-side.

## Database queries

- Every `$wpdb` query containing a variable must use `$wpdb->prepare()`. Never build
  a SQL string with string concatenation or interpolation.

## Capability checks

- Any function reachable from an admin-post handler, AJAX handler, or REST callback
  must call `current_user_can()` with a specific capability before performing any
  state-changing action. Never rely on a nonce check alone, a nonce proves the
  request came from your form, not that the requesting user is allowed to do this.

## Nonces

- Any form that changes state must include `wp_nonce_field()`, and the handler that
  processes it must verify with `check_admin_referer()` or `wp_verify_nonce()` before
  doing anything else.

## Hooks and functions

- Before using any hook or function name you are not certain exists, verify it
  against the WordPress core files in this project or developer.wordpress.org.
  See the "Why AI Coding Agents Hallucinate WordPress Hooks" lesson for why this
  matters.

## Coding standards

- Follow the WordPress PHP Coding Standards (WPCS): tabs for indentation, Yoda
  conditions for comparisons involving a constant, and space-padded parentheses,
  `if ( $x === true )` not `if ($x === true)`.
- If a PHPCS configuration with the WPCS ruleset exists in this repository, treat its
  rules as authoritative over general PHP style preferences.
Reference the real ruleset instead of restating it

WordPress Coding Standards is a maintained, installable PHPCS ruleset, not just documentation. If your plugin’s repo has a phpcs.xml referencing WordPress-Extra or WordPress-Docs, mention that file’s path in the skill so Claude checks against the actual configured rules rather than a general impression of WordPress style.

Step 4: test it with a deliberately risky prompt

Ask for something that would tempt an ungoverned agent to skip escaping or a capability check:

Add an admin-post handler that lets a logged-in user update their display
name from a form field, and show the updated name back on the same page.
What to check in the generated code
1
Is the incoming form value sanitized?
sanitize_text_field() or equivalent on the $_POST value before it touches anything.
2
Is there a capability check?
current_user_can(), not just a logged-in check, before the update runs.
3
Is there a nonce?
wp_nonce_field() in the form, check_admin_referer() in the handler.
4
Is the echoed-back name escaped?
esc_html() around the value when it's printed, even though it came from the same user.

Run this once with the skill absent from the repo, and once with it in place. The difference in the first draft Claude Code produces is the actual evidence the skill is doing something, not just documentation you wrote and never checked.

A skill in context doesn't guarantee every line follows it

Skill content loads into context and stays there, but a model can still drift, especially across a long session with a lot of other code already generated. Don’t treat “the skill is loaded” as equivalent to “this code is safe.” Lesson 8 in this course gives you the manual review checklist to run regardless of what guardrails were in place during generation.

Recap

A Claude Code skill at .claude/skills/wp-secure-code/SKILL.md, committed to your plugin’s repository, turns “please escape your output” from a one-off request into a standing rule every session and every teammate gets automatically. Write the rules as concrete, checkable statements referencing real WordPress functions, reference your actual WPCS ruleset where one exists, and verify the skill changes real output with a before/after test rather than trusting that writing it down was enough.

Resources & further reading

← Why AI Coding Agents Hallucinate WordPress Hooks Progressive Disclosure: Giving an Agent Just Enough Context →