How to Build a WordPress Duplicate Post Plugin Using Claude Code

Two prompts. A working plugin. Here’s exactly what I typed, what came back, and whether I’d do it again.

I’ve been curious about AI coding tools for a while — not in a “let’s replace developers” way, but in a practical sense. Can they actually save time on real projects? Or do you spend more time fixing the output than you would have spent writing it yourself?

So I picked a real project I needed done anyway — a WordPress plugin that adds a one-click “Duplicate” button to posts and pages — and I built the whole thing using Claude Code. No starter code, no templates. I just described what I wanted and watched what happened.

Here’s the full breakdown: the prompts I used, the code that came out, and what I actually think of the experience.

How to Build a WordPress Duplicate Post Plugin Using Claude Code

Prompt 1: Setting Up the Structure

I started simple. Before throwing a list of requirements at the AI, I wanted to see if it could scaffold a clean plugin skeleton — the file structure, the header comment, the basic boilerplate — without me having to guide it through every piece.

Prompt sent to Claude Code:

Create a basic WordPress plugin called Makitweb Duplicate. For now, just create the plugin structure and main PHP file. Don’t add any functionality yet.

That was the entire prompt. No file paths, no hints about what the header should contain. I wanted to see what it would figure out on its own.

Here’s what Claude Code did:

  • Created the makitweb-duplicate/ folder inside wp-content/plugins/
  • Created makitweb-duplicate.php with a complete plugin header (name, URI, description, version, author, license)
  • Added an ABSPATH guard to prevent direct file access
  • Defined three constants: MAKITWEB_DUPLICATE_VERSION, MAKITWEB_DUPLICATE_PATH, and MAKITWEB_DUPLICATE_URL
  • Used my details to populate the author field correctly

The output was exactly what a developer would write. No bloat, no unnecessary comments, no placeholder TODO blocks. Just a clean starting point.

One thing that stood out: it ran php -l on the file after creating it to verify there were no syntax errors. I didn’t ask for that. It just did it.


Prompt 2: The Full Functionality

This is where the real test was. I gave Claude Code a detailed requirements list — the kind of brief I’d write for another developer — and let it figure out the implementation.

Prompt sent to Claude Code:

Now implement the functionality for the Makitweb Duplicate plugin.

The plugin should add a “Duplicate” action to the Posts and Pages list in the WordPress admin.

Requirements:

  1. Add a “Duplicate” action for posts.
  2. Add the same action for pages.
  3. When clicked, create a new draft from the selected post or page.
  4. Copy the title and content.
  5. Copy the featured image if available.
  6. The original post or page must not be changed.
  7. Redirect the user to the new draft after duplication.
  8. Use proper WordPress hooks and APIs.
  9. Follow WordPress coding and security best practices.
  10. Do not modify WordPress core files.
  11. Keep the plugin lightweight and simple.

After implementing the functionality: show me which files were changed, briefly explain how it works, check the code for obvious errors, and tell me how I can test it.

Eleven requirements, plus four follow-up instructions. Let’s see what came back.


What Claude Code Built

The entire plugin ended up in under 80 lines of PHP, all in the single main file. Here’s the core of what it generated:

// Inject the Duplicate link into post and page rows
add_filter( 'post_row_actions', 'makitweb_duplicate_post_action', 10, 2 );
add_filter( 'page_row_actions', 'makitweb_duplicate_post_action', 10, 2 );

function makitweb_duplicate_post_action( $actions, $post ) {
    if ( ! current_user_can( 'edit_posts' ) ) {
        return $actions;
    }
    $url = wp_nonce_url(
        admin_url( 'admin.php?action=makitweb_duplicate&post_id=' . absint( $post->ID ) ),
        'makitweb_duplicate_' . $post->ID
    );
    $actions['makitweb_duplicate'] = '<a href="' . esc_url( $url ) . '">Duplicate</a>';
    return $actions;
}

A few things worth noting about the code quality. It didn’t use raw $_GET values anywhere without sanitising them first. Every user-facing string is wrapped in esc_html__(). The nonce is per-post — not a single global nonce — which means a stolen URL from one post can’t be replayed against another. And the redirect uses wp_safe_redirect() rather than a plain header() call.

These aren’t beginner-level mistakes to avoid. They’re the kind of security details that distinguish plugin code that would pass a WordPress.org review from plugin code that wouldn’t. Claude Code got all of them right without being asked.

How the duplicate action actually works

When you click the Duplicate link, WordPress fires the admin_action_makitweb_duplicate hook. The handler does this in order:

  1. Verifies the nonce — if it fails, execution stops with wp_die().
  2. Checks that the current user has edit_posts capability.
  3. Fetches the original post with get_post().
  4. Creates a new draft via wp_insert_post() — title, content, excerpt, post type, and author all copied over.
  5. Loops through every entry in get_post_meta() and copies it to the new post. This is what picks up the featured image: WordPress stores it as _thumbnail_id in post meta, so no special handling is needed.
  6. Redirects to the new draft’s edit screen using wp_safe_redirect().

The original post is never written to at any point.


Testing It

I activated the plugin and headed to Posts in my local WordPress install. The Duplicate link was there, sitting cleanly between Quick Edit and Trash on every row. Clicked it on a post with a featured image and a handful of custom fields — new draft appeared immediately with everything intact.

Then I tested the edge cases Claude Code told me to check: tampering with the nonce in the URL produces a “Security check failed” error page. Accessing the action URL while logged out redirects to the login screen (standard WordPress behaviour). A post with no featured image duplicates without any PHP warnings.

Everything worked on the first try. I didn’t touch the code.


What Worked Well, What to Know

What worked well: The security practices were solid without prompting — nonce verification, capability checks, sanitised inputs, safe redirect. It also stayed lightweight and didn’t build a settings page nobody asked for.

What to keep in mind:

  • Be specific with your prompts.
  • Listing requirements clearly — one per line — produced much tighter output than free-form descriptions would have.
  • And review the output before deploying. The code was good, but you still need to read and understand anything you put on a production site.

Full Code

<?php

/**
 * Plugin Name: Makitweb Duplicate
 * Plugin URI:  https://makitweb.com
 * Description: Duplicate posts and pages with a single click.
 * Version:     1.0.0
 * Author:      Yogesh Singh
 * Author URI:  https://makitweb.com
 * License:     GPL-2.0+
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: makitweb-duplicate
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
     exit;
}

// Plugin version.
define( 'MAKITWEB_DUPLICATE_VERSION', '1.0.0' );

// Plugin directory path.
define( 'MAKITWEB_DUPLICATE_PATH', plugin_dir_path( __FILE__ ) );

// Plugin directory URL.
define( 'MAKITWEB_DUPLICATE_URL', plugin_dir_url( __FILE__ ) );

/**
 * Add "Duplicate" action link to post rows.
 */
function makitweb_duplicate_post_action( $actions, $post ) {

    if ( ! current_user_can( 'edit_posts' ) ) {
        return $actions;
    }

    $url = wp_nonce_url(
        admin_url( 'admin.php?action=makitweb_duplicate&post_id=' . absint( $post->ID ) ),
        'makitweb_duplicate_' . $post->ID
    );

    $actions['makitweb_duplicate'] = '<a href="' . esc_url( $url ) . '">' . esc_html__( 'Duplicate', 'makitweb-duplicate' ) . '</a>';

    return $actions;

}

add_filter( 'post_row_actions', 'makitweb_duplicate_post_action', 10, 2 );
add_filter( 'page_row_actions', 'makitweb_duplicate_post_action', 10, 2 );

/**
 * Handle the duplicate action.
 */
function makitweb_duplicate_handle() {

    $post_id = isset( $_GET['post_id'] ) ? absint( $_GET['post_id'] ) : 0;

    if ( ! $post_id ) {
         wp_die( esc_html__( 'Invalid post ID.', 'makitweb-duplicate' ) );
    }

    if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'makitweb_duplicate_' . $post_id ) ) {
         wp_die( esc_html__( 'Security check failed.', 'makitweb-duplicate' ) );
    }

    if ( ! current_user_can( 'edit_posts' ) ) {
         wp_die( esc_html__( 'You do not have permission to duplicate posts.', 'makitweb-duplicate' ) );
    }

    $original = get_post( $post_id );

    if ( ! $original ) {
         wp_die( esc_html__( 'Post not found.', 'makitweb-duplicate' ) );
    }

    // Insert the duplicate as a draft.
    $new_post_id = wp_insert_post(
         array(
             'post_title'   => $original->post_title,
             'post_content' => $original->post_content,
             'post_excerpt' => $original->post_excerpt,
            'post_type'    => $original->post_type,
            'post_status'  => 'draft',
            'post_author'  => get_current_user_id(),
         )
    );

    if ( is_wp_error( $new_post_id ) ) {
         wp_die( esc_html__( 'Could not create duplicate.', 'makitweb-duplicate' ) );
    }

    // Copy all post meta, including the featured image (_thumbnail_id).
    $meta_entries = get_post_meta( $post_id );
    if ( $meta_entries ) {
         foreach ( $meta_entries as $meta_key => $meta_values ) {

             foreach ( $meta_values as $meta_value ) {
                 add_post_meta( $new_post_id, $meta_key, maybe_unserialize( $meta_value ) );
             }
         }
    }

    // Redirect to the new draft's edit screen.
    wp_safe_redirect( admin_url( 'post.php?action=edit&post=' . $new_post_id ) );
    exit;

}

add_action( 'admin_action_makitweb_duplicate', 'makitweb_duplicate_handle' );

Is This How I’ll Build WordPress Plugins Going Forward?

For something like this — a contained, well-defined feature where the WordPress API patterns are well established — honestly, yes. It’s not that Claude Code is doing something magical. It knows the WordPress API, it knows what secure PHP looks like, and it can execute a clear brief. That combination is genuinely useful.

What it doesn’t replace is the thinking. Writing that second prompt — breaking down the requirements, being explicit about security expectations, asking for a post-build explanation — that’s still work. The more precise you are going in, the better the output is coming out.

The plugin is up and running. Watch the video above if you want to see the full session — every prompt, every output, in real time.

The full plugin code is available on GitHub if you want to use it or build on it: https://github.com/makitweb/makitweb-copy-id

Leave a Comment