— Article — № 004

004 —WordPress

wp-config.php on production: a safe edit procedure

wp-config.php is one line away from a white screen. Here is the procedure we use to change it on a live WordPress site without ever dropping a request.

Brass padlock with wax-sealed paper tag on folded linen cloth, bone paper surface, dark oak table edge.
Hero · staged still№ 004

It is 14:00 on a Wednesday. A client has emailed asking why their staging URL is leaking into Google. You open SFTP, find wp-config.php, and realise the WP_HOME constant is hardcoded to the staging domain. You need to change it, and the site is currently serving roughly forty requests a minute.

wp-config.php is the WordPress file with the worst risk-to-edit ratio in the whole CMS. One stray character (a missing semicolon, a smart quote pasted from a Notion doc, a stray byte order mark) takes the entire site to a white screen or a half-rendered HTML dump that exposes the absolute file path. There is no warning, no soft failure, no retry. PHP fails to parse and the site is down until you revert or fix.

This is the procedure we use when editing wp-config.php on a live WordPress site without dropping requests. It assumes SFTP access, a shell on the server is nice but not required, and you have php installed on your laptop for a lint pass.

What actually breaks

Before the procedure, it helps to know the specific ways a wp-config.php edit takes a production site down. In rough order of frequency, in our experience:

  • A stray closing ?> tag with trailing whitespace, which corrupts the Content-Type header and breaks redirects.
  • A define() for a constant that is already defined further down, which throws a warning that some hardened PHP 8 configs surface as a fatal.
  • Smart quotes from a copy-paste ( instead of ") which the PHP parser cannot read.
  • A BOM at the top of the file from an editor that defaulted to UTF-8-with-BOM, sending three invisible bytes before <?php and breaking header() calls everywhere.
  • The $table_prefix line moved below the require_once call to wp-settings.php, which silently fails because WordPress has already booted.

None of these are caught by visual inspection. All of them are caught by php -l or by reading the raw bytes of the file. We do both.

The pre-flight

The procedure assumes one rule: you never edit wp-config.php in place over SFTP. You always edit a copy, lint it, swap it, and keep the previous copy reachable for a day in case the new file surfaces a runtime error.

Pull the live file down with a timestamped name:

sftp user@host:/var/www/site/wp-config.php ./wp-config.live.php
cp wp-config.live.php wp-config.bak.2026-05-12.php
cp wp-config.live.php wp-config.new.php

The .bak copy never gets touched again. The .new copy is where you make the edit. Open it in an editor you trust to not insert a BOM and not normalise quotes. VS Code with the status-bar encoding visible is fine. TextEdit on macOS in rich-text mode is not.

Make the change. Then run a lint pass locally:

php -l wp-config.new.php
# No syntax errors detected in wp-config.new.php

If php -l returns anything other than that exact line, stop. Do not upload. The PHP parser on the server is the same parser, and it will fail identically. The php command-line options page spells out that -l reads the file, parses it, and exits without executing any code, which makes it safe to run on a config that contains real database credentials.

The lint pass catches roughly 90% of what kills wp-config.php edits. The remaining 10% is runtime: a constant defined twice, an environment variable that exists on your laptop but not on the server, a require path that resolves on macOS but not on the Linux box. For those, you need the swap.

The atomic swap

SFTP does not give you an atomic rename across writes the way mv on POSIX does. Uploading a file with the same name overwrites it byte by byte, and there is a window (small but real) where the file is half-written and PHP can read it. If a request lands during that window, you get a parse error and the request 500s.

The way around this is to upload under a different name and rename on the server. Most SFTP servers honour rename as a single filesystem operation.

sftp user@host <<'EOF'
cd /var/www/site
put wp-config.new.php wp-config.php.incoming
rename wp-config.php wp-config.php.previous
rename wp-config.php.incoming wp-config.php
EOF

For the duration of the two renames (milliseconds on ext4 or APFS), wp-config.php either points at the old file or the new one. It never points at a half-written file. If the new file has a runtime problem, you have wp-config.php.previous sitting next to it, and a single rename puts it back.

After the swap

Hit the site. Not just the homepage. Hit a wp-admin URL, a REST endpoint, a page with a known plugin shortcode. The reason: wp-config.php is loaded before any plugin code, but plugin code can depend on constants you just changed. WP_DEBUG flipping from false to true will surface deprecated notices from a plugin author who shipped PHP 7.4 syntax in 2024. Better to see them now than at 23:00.

Once the site is responding cleanly, lock the file down:

chmod 440 wp-config.php
chown www-data:www-data wp-config.php

440 means the web server user can read it and nobody can write it. 400 is stricter but breaks some shared-hosting setups where the web user is also the FTP user. Pick the tighter of the two that does not break your stack. The WordPress hardening guide recommends 400 or 440 and explicitly warns against 644 or 666.

Keep wp-config.php.previous on the server for the rest of the day. Delete it tomorrow. If you forget, your next deploy will probably trip over it, which is its own form of reminder.

Credentials and key rotation

If the edit included rotating the eight AUTH_KEY / SECURE_AUTH_KEY / LOGGED_IN_KEY / NONCE_KEY salts (plus their four _SALT counterparts), every currently logged-in user gets logged out. That is the design. If you are rotating because of a suspected compromise, do it now. If you are rotating because the codex says to do it periodically and your client has four thousand logged-in subscribers, schedule it for a quiet hour and post a banner.

Generate the new salts from the official WordPress salt API rather than rolling your own. It returns 64 cryptographically random bytes per constant in exactly the format wp-config.php expects. Paste, lint, swap.

If the edit changed database credentials (DB_USER, DB_PASSWORD, DB_HOST), test the new credentials with the mysql client from the same server first. WordPress will not give you a useful error if the credentials are wrong. It gives you Error establishing a database connection, which could mean the credentials are wrong, the DB is down, the host is unreachable, or you typoed a constant name. mysql -u newuser -p -h localhost tells you which of the four it is in under a second.

When the procedure does not fit

This procedure assumes SFTP, a shell habit, and a php binary on your laptop. For a legacy site that you inherited last week, where the SFTP credentials are in a 2019 email and the previous developer used FileZilla's drag-and-drop, the procedure is hard to enforce by hand. That is the case we kept hitting at our own shop, and it is the reason we ended up building Pier: a Mac app that docks with the FTP/SFTP server and treats every file edit as a tracked, revertible change. The wp-config.php swap above is one keystroke in Pier, with the lint pass and an automatic version history entry that makes go back to what was there at 14:00 a literal one-click action. Same procedure, less rope.

The smallest thing you can do today: pull a copy of your production wp-config.php, run php -l on it locally, and confirm it parses clean. If it does not, you have a latent bug waiting for the next edit to expose it.

— Questions —

Can I edit wp-config.php from the WordPress admin?

No. There is no built-in admin UI for it. Some security plugins surface a read-only view, but write access is intentionally filesystem-only for hardening reasons.

What file permissions should wp-config.php have?

440 in most setups, 400 if your stack allows it. Never 644 or 666. The web server user needs read, nobody needs write at runtime.

Will rotating the salts log every user out?

Yes. That is the entire point of the salts. Schedule the rotation for a low-traffic window and post a banner if your site has many logged-in users.

Does php -l catch every possible wp-config.php error?

It catches syntax errors only. Runtime issues like a duplicate define, a wrong DB host, or a missing require path will parse cleanly and fail at boot.