The rule in my own documentation was backwards, and following it never hurt

By John Patzi · Published · 8 min read

Three cases measured: with no user flag WP-CLI removes the kses filters and nothing is stripped, with an administrator named nothing is stripped either, and only naming a user without the unfiltered_html capability would apply the filters

The short version. For months my own notes carried a rule: always pass --user=1 to WP-CLI, because without it WordPress strips <style> and <link> from the post. I followed it on every command. I cited it to a colleague. I nearly published it.

Then I measured it. Nothing is stripped without the flag. The mechanism runs the other way round: the flag is the only thing in that sentence that could ever turn the filtering on.

The rule was not wrong about a risk being real. It was wrong about which direction the risk points, which is worse, because following it felt like safety.

What the rule said, and what I did about it

The line lived in the project’s own instructions, in a list of lessons already paid for:

wp post update without --user=1 deletes <style> and <link> (kses)

That is a specific, actionable, plausible claim, and I treated it as settled. It shaped how I wrote every command that touched a post for months. It is exactly the kind of note you write after something goes wrong once and never re-examine, because re-examining it costs an afternoon and the workaround costs nine characters.

The measurement

A draft post with one <style> block and one <link> tag, updated three ways. The post was never published and was deleted afterwards.

Command<style><link>
create, no --userkeptkept
update, no --userkeptkept
update with --user=1keptkept
update with a user ID that does not existcommand aborts, nothing written

WordPress 7.1, WP-CLI 2.12.0. The rule predicted the second row would lose both tags. It lost neither.

Three passes, no stripping anywhere. The prediction failed on the exact case it was written for.

Three cases measured: with no user flag WP-CLI removes the kses filters and nothing is stripped, with an administrator named nothing is stripped either, and only naming a user without the unfiltered_html capability would apply the filters
The third column is the one I never tested, and I have marked it as read rather than measured. It is also the only column where the risk in my rule actually lives.

Why it runs backwards

The filtering in question is set up by kses_init(), which the reference describes as removing all the filters first “in case the current user does not need to have KSES” and then adding them back if they do. WordPress hooks it to both init and set_current_user.

So whether your content is filtered depends on who WordPress thinks is making the request. And that is precisely what the --user flag sets — the WP-CLI configuration reference documents it in one line: “Set the WordPress user.”

No user named: the filters come off

Reading WP-CLI’s own runner, the --user handling has an else branch, and the else branch removes the kses filters at init priority 11 — after WordPress has installed them. With no user in play there is no capability to check, and the tool takes the filters off rather than leaving a command-line invocation subject to rules designed for a web form.

A user named: the filters follow that user’s capability

Name somebody and WordPress sets the current user, which fires set_current_user, which runs kses_init() again — this time with a real user whose capabilities decide the outcome. The capability is unfiltered_html, described in the roles and capabilities reference as allowing a user to “post HTML markup or even JavaScript code”. Administrators have it on a single site. Editors and authors do not.

So the dangerous command is not the one missing --user. It is the one naming a user without that capability. My rule told me to add the flag to stay safe, when the flag is the only part of the command that can introduce the filtering at all.

On this site the flag happens to be harmless, because user 1 is an administrator and holds the capability — which is exactly why the rule survived so long. It never produced a wrong outcome. It just described the wrong mechanism, and would have led me somewhere bad the first time I pointed it at a non-admin.

What I cannot tell you

Two limits, because this is the kind of claim that deserves them.

I have not verified that no plugin on this site calls wp_set_current_user() later in the request than the point where the filters are removed. If one did, it would re-run the initialisation with whatever user it named and the outcome could differ. My measurement covers this site with this set of plugins, not WordPress in general.

And there is a case I did not test: naming a user who lacks the capability. Doing that properly means creating a non-admin account on a production site to strip tags off a post, which is not a thing I am going to do to demonstrate a point. The reading of the code says it would strip; I have not seen it strip. That distinction is the whole reason this note exists, so I am not going to blur it in my own favour.

Checking that a site does what its own documentation claims — including the parts nobody has reread in months — is the habit behind the free check, which reports what your pages actually return rather than what they should. The long version, across the stack, is the technical SEO and server work.

The real bug was three characters away

The rule came up because of a maintenance script: a safety net that publishes scheduled posts which got stuck. Its rescue line reads, in outline:

What happensWhat the log records
Rescue runs, post published“RESCUE: post 154 published”
Rescue runs, command failsnothing at all
Nothing needed rescuingnothing at all

The last two rows are indistinguishable from outside, and one of them is a failure.

The line uses shell’s &&, which the bash reference defines exactly: the second command “is executed if, and only if, command1 returns an exit status of zero”. Success is logged. Failure writes nothing, so a broken safety net looks identical to a quiet one.

That is the defect worth fixing, and I went looking for it because I was chasing a different one that turned out not to exist. The tag-stripping risk I spent the afternoon on was imaginary. The silence-on-failure risk sitting in the same three lines was real, and I would not have seen it if the first suspicion had been correct.

Why an old note is more dangerous than no note

A rule you wrote yourself, from a real incident, in your own documentation, carries more authority than anything you read elsewhere — and gets audited less than anything you read elsewhere. Nobody re-reads their own lessons file looking for lies.

It also had the best possible camouflage: following it never hurt. Adding the flag on a site where user 1 is an administrator produces identical results either way. A rule that is wrong and costly gets caught in a week. A rule that is wrong and free can sit there indefinitely, being cited.

I found this one because I set out to write about it. That is not a repeatable process. The repeatable version is smaller: when you are about to explain a mechanism to somebody else, check it rather than recite it — the explaining is when you finally have to be specific about how a thing works, and specificity is where a wrong rule stops fitting.

The full method, and the places where it stops being able to tell you anything, is in how I measure. If you would rather have your own stack checked than read about mine, send me a domain.

WP-CLI, kses and scheduled posts: frequently asked questions

Does WP-CLI strip HTML if I do not pass --user?

Not in my measurement. With no user named, WP-CLI removes the kses filters after WordPress installs them, so content passes through unfiltered. Naming a user re-runs the initialisation and the outcome then depends on that user’s capabilities.

So when does content actually get stripped?

When the current user lacks unfiltered_html. On a single-site install administrators have it; editors and authors do not. Naming a non-admin is the case to be careful with.

What happens with --user and an ID that does not exist?

The command aborts with an invalid-user error and writes nothing. That is a safe failure, but it is still a failure — if it happens inside a script whose logging only records success, nobody finds out.

Why would a scheduled post need rescuing at all?

Because the publish event can be lost from the scheduled task list while the post sits in a future state with a date already past. WordPress’s cron only runs when something triggers it, so a low-traffic site can leave such a post unpublished indefinitely. Note that “missed schedule” is a label the dashboard paints, not an error code you can look up.

How do I know my safety net still works?

Make it log both branches. If it only records success, an unfired net and a broken net produce the same output, and you will believe the first one.

Should I stop passing --user=1?

No — on a site where user 1 is an administrator it is harmless, and being explicit about who is acting is good practice in a script. Just do not believe it is protecting you from something it does not protect you from.

The general shape of this

Six notes on this site now end the same way: something I was confident about turned out to be a description of a mechanism I had never checked. The pattern is consistent enough to be worth stating plainly. Confidence accumulates in the places that never produce a visible failure, because those are the only places where being wrong is comfortable.

The practical version is not scepticism, which is exhausting and mostly useless. It is narrower: when you are about to write a rule down, or cite one you wrote before, that is the moment it is cheapest to test. Not when it breaks — it may never break — but when you are about to hand it to somebody else as true.

The line is still in my notes. It now says the opposite, with the measurement next to it.

The same tool produced a different surprise when a post I meant to schedule published immediately instead.