First-time setup

This guide takes you from nothing to a working connection between Claude Code and a WordPress site. You will create an application password, keep it in a local file, add a small helper script, and run a read test and a write test. It takes a few minutes if your site is already online.

What you need

  • A WordPress site served over HTTPS. Application passwords have been part of WordPress core since version 5.6, and WordPress offers them on HTTPS sites.
  • An administrator or editor account on that site.
  • Node.js 18 or newer on your computer.
  • Claude Code, installed on your computer.

A sandbox site is the right place to practise. Nothing you do there matters to anyone, so you can find out what works before you try it on a site that does.

Step 1: Create an application password

  1. In wp-admin, go to Users → Profile.
  2. Scroll down to the Application Passwords section.
  3. Type a name for the password, such as Claude.
  4. Click Add New Application Password.
  5. Copy the password that is shown. It is displayed once only. It is 24 characters in groups of four, and the spaces may stay.

Step 2: Store the connection details in a .env file

Create an empty project folder on your computer. In it, create a file named .env with three lines:

WP_URL=https://example.com
WP_USER=your-wordpress-username
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx

Replace the three values with your site address, your WordPress username and the application password from step 1.

Never paste the password into the chat. It belongs in .env and nowhere else. If the folder is ever put under git, add .env to .gitignore before the first commit.

Step 3: Add the helper script

Create a folder named tools in the project, and save the following as tools/wp.mjs.

#!/usr/bin/env node
// Sends one WordPress REST API request. Zero dependencies (Node 18+).
// Usage: node tools/wp.mjs <METHOD> <path-without-leading-slash> [json-body | @file.json]
import { readFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const usage = 'Usage: node tools/wp.mjs <METHOD> <path-without-leading-slash> [json-body | @file.json]  (e.g. GET "wp/v2/pages?per_page=5")';
const [method, rawPath, bodyArg] = process.argv.slice(2);
if (!method || !rawPath) {
  console.error(usage);
  process.exit(2);
}
if (/^[A-Za-z]:[\\/]/.test(rawPath)) {
  console.error('Path looks like a Windows path: the shell rewrote the leading slash. Write the path without a leading slash, e.g. wp/v2/pages');
  process.exit(2);
}

const envPath = join(dirname(fileURLToPath(import.meta.url)), '..', '.env');
if (!existsSync(envPath)) {
  console.error('Missing file: .env (copy .env.example to .env and fill it in)');
  process.exit(2);
}

const env = {};
for (const line of readFileSync(envPath, 'utf8').split(/\r?\n/)) {
  const t = line.trim();
  if (!t || t.startsWith('#')) continue;
  const i = t.indexOf('=');
  if (i < 1) continue;
  const key = t.slice(0, i).trim();
  let val = t.slice(i + 1).trim();
  if (val.length >= 2 && (val[0] === '"' || val[0] === "'") && val.endsWith(val[0])) {
    val = val.slice(1, -1);
  }
  env[key] = val;
}
for (const name of ['WP_URL', 'WP_USER', 'WP_APP_PASSWORD']) {
  if (!env[name]) {
    console.error(`Missing variable in .env: ${name}`);
    process.exit(2);
  }
}

let body;
if (bodyArg !== undefined) {
  if (bodyArg.startsWith('@')) {
    const file = bodyArg.slice(1);
    try {
      body = readFileSync(file, 'utf8');
    } catch {
      console.error(`Cannot read body file: ${file}`);
      process.exit(2);
    }
  } else {
    body = bodyArg;
  }
}

const url = `${env.WP_URL.replace(/\/+$/, '')}/wp-json${rawPath.startsWith('/') ? '' : '/'}${rawPath}`;
const auth = Buffer.from(`${env.WP_USER}:${env.WP_APP_PASSWORD}`).toString('base64');
const headers = { Authorization: `Basic ${auth}`, Accept: 'application/json' };
if (body !== undefined) headers['Content-Type'] = 'application/json';

let res;
try {
  res = await fetch(url, { method: method.toUpperCase(), headers, body });
} catch (e) {
  console.error(`Request failed: ${e?.cause?.code || e?.name || 'error'}`);
  process.exit(1);
}

console.error(`HTTP ${res.status}`);
if (method.toUpperCase() === 'GET') {
  for (const h of ['X-WP-Total', 'X-WP-TotalPages']) {
    const v = res.headers.get(h);
    if (v !== null) console.error(`${h}: ${v}`);
  }
}
const text = await res.text();
try {
  console.log(JSON.stringify(JSON.parse(text), null, 2));
} catch {
  if (text) console.log(text);
}
process.exit(res.ok ? 0 : 1);

The script reads .env and sends one authenticated request to <site>/wp-json/<path> using HTTP Basic authentication. It prints the HTTP status and the JSON response, and it never prints the password. It has no dependencies beyond Node.js.

Step 4: Tell Claude the rules

In the same project folder, create a short instruction file named CLAUDE.md. Claude Code reads it at the start of a session. Keep it brief. It only needs to say four things:

  • the address of the site;
  • that the site is a sandbox;
  • that tools/wp.mjs is the only thing that reads .env;
  • that Claude must never open or print the .env file.

For example:

# My WordPress sandbox

- Site: https://example.com (a sandbox, not a live site).
- Work on the site through the REST API: node tools/wp.mjs <METHOD> <path> [json-body | @file.json]
- Write paths without a leading slash, for example wp/v2/pages.
- Only tools/wp.mjs reads .env. Never open, read or print the .env file.

Step 5: Run the read test

Open Claude Code in the project folder and ask it to run the test, or run it yourself in a terminal:

node tools/wp.mjs GET "wp/v2/pages?per_page=5"

You should see HTTP 200 followed by a list of pages as JSON. If you do, the address, the username and the application password are all correct.

Step 6: Run the write test

Create a small file named hello.json that describes a draft page:

{"title":"Hello from Claude","status":"draft","content":"<!-- wp:paragraph -->\n<p>Connection test.</p>\n<!-- /wp:paragraph -->"}

Then send it:

node tools/wp.mjs POST wp/v2/pages "@hello.json"

You should see HTTP 201 and the new page as JSON. In wp-admin, open Pages: the draft titled “Hello from Claude” is there. You can delete it, or leave it as a reminder of the first successful write.

From here you can give Claude real instructions in plain words, and it will use the helper to carry them out.

Troubleshooting

404 with the code rest_no_route

On Windows with Git Bash, an argument that starts with a slash, such as /wp/v2/pages, is rewritten by the shell into a Windows path, and the site answers 404 rest_no_route. Write the path without the leading slash: wp/v2/pages.

401

The username or the application password is wrong, or application passwords are disabled on the site, for example by a security plugin.

403

The login works, but the account’s role is not allowed to do that action. Use an account with a role that can do the work.

Safety

  • You can revoke an application password at any time from the same Profile screen.
  • An application password does not expose the account’s real login password.
  • Consider a dedicated user with only the role the work needs.
  • Practise on a sandbox before you connect a site that matters.

What comes next

You now have a working connection. Go back to the home page for an overview of what you can ask Claude to do, and for the guides that follow.