Skip to content

New WordPress Site from the Template

Clone a ready-made WordPress site instead of building one from scratch. One command produces a live, TLS-secured, hardened site with an empty theme ready to build on.

/new-wp <name>

That creates <name>.franzvoid.is: docroot, database, files and content cloned from the template, a hardened Apache vhost, a Let's Encrypt certificate, and an active empty theme called <name>.

For building a site from nothing, see Set up a WordPress site — that page is still the reference for how each piece works.

Why this exists

thingvad.franzvoid.is was left sitting at the browser install wizard in July 2026. On 1 August a stranger completed the wizard, made themselves an admin, and ran a Monero miner on the box for eight days. Every step below is arranged so a WordPress docroot is never reachable over HTTP until it is fully installed. Full write-up: incident report INC-20260801-THINGVAD; evidence archive at /root/incident-thingvad-20260801.tar.gz.

The template site

https://template.franzvoid.is/ — a normal WordPress site. Log in with the credentials in /var/www/.env and change whatever you like.

Docroot /var/www/template.franzvoid.is
Database template_wp
Admin DEFAULT_WP_USER / DEFAULT_WP_PASS from /var/www/.env (root-only, mode 600)
Seeded Icelandic timezone, /%postname%/ permalinks, comments closed, no third-party plugins, three hardening mu-plugins
Indexing Discouraged, and the vhost sends X-Robots-Tag: noindex

What propagates and what doesn't

Everything in the template's files and database is copied into every site created afterwards — plugins, settings, pages, media, users, menus.

Design does not propagate. /new-wp activates the empty <name> theme, and block-theme templates plus Global Styles are stored per-theme, so they are orphaned on activation. Treat the template as content and configuration; design starts fresh in each site's own theme.

Keep it lean

Every upload and post in the template is copied into every future site, forever. Audit it before a batch of new sites.

Naming

<name> must be 2–26 characters, lowercase a-z0-9-, starting with a letter. Underscores are rejected on purpose.

You type Domain Database + user Theme
haukar haukar.franzvoid.is haukar_wp haukar
my-site my-site.franzvoid.is my_site_wp my-site

Hyphens are kept in the domain and theme slug and converted to underscores only for the database. Underscores are forbidden in the name itself because otherwise my-site and my_site would silently collide on the same database.

Reserved names (www, mail, api, template, every existing site, and thingvad) are refused.

What is cloned and what is regenerated

Cloned from the template Regenerated per site
WordPress core, at the template's exact version Database name, user and password
Plugins and mu-plugins Authentication salts (wp config shuffle-salts)
All content, media, options, users siteurl / home / site title
.htaccess and permalink structure Apache vhost and Let's Encrypt certificate
Admin password (hashes are self-salted, so it carries over) The <name> theme, activated

Fresh salts matter: reusing the template's would mean every site shares auth-cookie signing keys, and a session cookie from one site could validate on another.

Security posture

Every site the skill creates gets these, and so does the template.

The installer is never exposed. WordPress is installed entirely over WP-CLI while no vhost exists. The script refuses to run a2ensite unless wp core is-installed succeeds, and after provisioning it asserts from the outside that /wp-admin/install.php does not return 200. A 200 there means the site is claimable by anyone and the provision has failed.

vhost deny rules (written before certbot runs, so they propagate into the -le-ssl.conf it generates):

Blocked Why
.git/ and any .git* file A scanner cloned https://franzvoid.is/.git/ on 2026-08-01
install.php The thingvad failure mode
.php/.phar/.sh/etc. under wp-content/uploads, including double extensions like evil.php.jpg The thingvad dropper was placed under wp-content and run with a direct GET
readme.html, license.txt, wp-config-sample.php, dotfiles Version fingerprinting

mu-plugins in wp-content/mu-plugins/:

  • disable-user-enumeration.php — removes the wp-json user endpoints and the ?author=N redirect
  • disable-xmlrpc.php — turns off xmlrpc.php and stops advertising it
  • harden-uploads.php — rejects executable uploads at the application layer too, in case the docroot is ever moved to a vhost without the deny block

The firewall is never touched. ufw is default-deny inbound with only 22/80/443 open. The scripts check that it is active and refuse to run if it isn't, but contain no ufw or iptables commands at all.

Verifying a new site

F=<name>.franzvoid.is; D=/var/www/$F
T() { sudo -u www-data wp --path="$D" "$@"; }

curl -s -o /dev/null -w "install %{http_code}\n" "https://$F/wp-admin/install.php"   # 403 — never 200
curl -s -o /dev/null -w "git     %{http_code}\n" "https://$F/.git/config"            # 403
curl -s -o /dev/null -w "http    %{http_code}\n" "http://$F/"                        # 301
curl -s -o /dev/null -w "https   %{http_code}\n" "https://$F/"                       # 200
curl -s -o /dev/null -w "users   %{http_code}\n" "https://$F/wp-json/wp/v2/users"    # 404

T option get siteurl                                      # https://<name>.franzvoid.is
T theme list                                              # <name> active
T db query "SELECT COUNT(*) FROM wp_options WHERE option_value LIKE '%template.franzvoid.is%';"   # 0
sudo apache2ctl configtest                                # Syntax OK
getfacl -p "$D" | grep -E '^(user:franz|default:user:franz)'

curl and SNI

When testing a vhost from the server itself, use curl --resolve host:443:127.0.0.1 https://host/. Using -H "Host: ..." against localhost returns 421 Misdirected Request, because the TLS SNI name doesn't match.

New sites inherit the template's "discourage search engines" setting. WordPress 7 implements that with a <meta name="robots" content="noindex, nofollow"> tag — not with Disallow: / in robots.txt, so don't look for it there.

When the site is ready:

sudo -u www-data wp --path=/var/www/<name>.franzvoid.is option update blog_public 1

If something goes wrong

The script refuses rather than clobbers. If the docroot, vhost, database or database user already exists, it exits with code 2 and creates nothing. There is no --force: to rebuild a site, tear it down first.

On a failure partway through it rolls back automatically, unwinding in reverse: disable and remove the vhost, reload Apache, drop the database and user, delete the docroot. Certificates are deliberately left behind — an orphan cert is harmless, and Let's Encrypt allows only 5 duplicate certificates per week, which a rollback-retry loop would burn through.

Use --keep-on-failure to leave the wreckage for inspection.

Warning

Don't background a /new-wp run and then quit Claude. claude-sudo removes its sudoers rule on exit, and the rollback needs sudo too.

Removing a site

~/.claude/skills/new-wp/scripts/teardown-wp.sh --yes-i-mean-it <name>

Drops the database and rm -rfs the docroot. It prints exactly what it will remove before doing anything, and hard-refuses on the template and on every pre-existing site, so it can only delete something /new-wp created.

Files

Path
~/.claude/skills/new-wp/SKILL.md The /new-wp skill
~/.claude/skills/new-wp/scripts/new-wp.sh Provisioning engine — runs standalone, no Claude needed
~/.claude/skills/new-wp/scripts/teardown-wp.sh Guarded undo
~/.claude/skills/new-wp/assets/vhost.conf.tmpl The hardened vhost, __FQDN__ placeholders
~/.claude/skills/new-wp/assets/mu-plugins/ The three hardening mu-plugins
~/.claude/skills/new-wp/assets/theme/ Starter theme sources
/var/www/.wp-cli/config.yml Tells WP-CLI mod_rewrite exists, so wp rewrite --hard can write .htaccess

new-wp.sh is a plain bash script — you can run it from a normal shell without Claude:

~/.claude/skills/new-wp/scripts/new-wp.sh --dry-run <name>   # preflight only
~/.claude/skills/new-wp/scripts/new-wp.sh --staging <name>   # test cert, saves quota
~/.claude/skills/new-wp/scripts/new-wp.sh <name>

The starter theme

wp-content/themes/<name>/ — six files, deliberately unstyled:

style.css (theme header plus a minimal reset and a centered wrapper), header.php, footer.php, index.php, functions.php, and a generated screenshot.png with the site name on it.

It renders a valid HTML document with wp_head() and wp_footer() firing and its own stylesheet enqueued — so it works out of the box and you extend from there. functions.php registers a primary menu location and the usual theme supports.