Problem
When the install/upgrade flow runs with RUN_AS_ROOT=true (the elif branch in lib/detect.sh), the resulting kimaki.service systemd unit gets User=root. This is the entry point for a long chain of root-ownership inheritance that breaks WordPress auto-update over time.
The cascade
-
kimaki.service runs as root → opencode workers spawned by kimaki run as root → wp invocations run as root → every file PHP/WP-CLI writes is owned root:root mode 0644.
-
data-machine-code composes AGENTS.md under root → its datamachine_code_resolve_wp_cli_cmd() correctly detects posix_geteuid() === 0 and bakes wp --allow-root into every example. Coding agents then dutifully use --allow-root for every command, which keeps the root-write loop going.
-
WP-CLI core upgrade (wp core update) run by an agent as root creates new files in wp-admin/, wp-includes/, and bundled plugin/theme dirs as root:root. The next time WordPress tries to auto-update those bundled plugins (akismet, etc.) via the PHP-FPM www-data user, it fails with copy_failed_for_update_core_file / copy_failed_copy_dir_plugins because www-data can't overwrite root-owned files.
-
DM image processing, OG card generation, daily memory writes, agent file scaffolding — all create root:root files inside wp-content/uploads/ because they run inside the same root-process tree.
Real-world impact (extrachill.com production, May 1-2 2026)
- After WP 7.0-RC2 manual upgrade ran via
wp --allow-root core update, ~6,000 root-owned files landed across wp-admin/, wp-includes/, and bundled plugin dirs (akismet, breeze, easy-wp-smtp, gutenberg, plugin-check, twentytwentyX themes).
- Two consecutive WordPress auto-update emails arrived with
copy_failed_* errors before the underlying ownership/permission state was caught and fixed manually.
- ~10,000 root-owned files accumulated in
wp-content/uploads/ over previous coding-agent sessions, contributing background bloat.
Why the non-root path exists but isn't used
lib/detect.sh lines 164-180 define three modes:
if [ "$LOCAL_MODE" = true ]; then
SERVICE_USER="$(whoami)" # local install, current user
elif [ "$RUN_AS_ROOT" = true ]; then
SERVICE_USER="root" # the trap
else
SERVICE_USER="opencode" # default: dedicated non-root user
fi
The default branch correctly creates the opencode system user (added to www-data group in lib/infrastructure.sh), chowns KIMAKI_DATA_DIR to it, and templates User=opencode into all systemd units. This is exactly the right architecture.
The problem is RUN_AS_ROOT=true is too easy to enable, and once enabled, every downstream artifact (systemd unit, file ownership, AGENTS.md instructions) bakes in the root assumption. There's no recovery path short of full reinstall + re-chown.
Proposed fixes (any subset)
A. Deprecate RUN_AS_ROOT=true or hide it behind an explicit flag
The non-root path works correctly and is already the default. The root path exists for what use case? If it's "user can't be bothered to create a system user," that's a setup convenience that creates long-term operational damage. Consider:
- Removing
RUN_AS_ROOT entirely
- Or renaming it to something explicit like
KNOWN_BAD_RUN_AS_ROOT=true that signals the operator is choosing pain
- Or printing a giant warning when this branch is taken: "WARNING: RUN_AS_ROOT=true selected. This will create root-owned files throughout your WordPress install. WordPress auto-updates will fail. Use the default non-root path unless you know exactly why you need this."
B. Add a kimaki doctor / repair subcommand
For installations already in the RUN_AS_ROOT=true state, provide an in-place migration:
kimaki repair --switch-to-non-root
That would:
- Create the
opencode user if missing
- Chown
KIMAKI_DATA_DIR, DM_WORKSPACE_DIR, and the WordPress install (or at least wp-admin/, wp-includes/, and bundled plugins) to the new user
- Update systemd unit
User= directive
- Fix file mode on root-owned WP files (chmod g+w so www-data can still update them during transition)
daemon-reload and restart kimaki
C. Detect the bad state at upgrade time
upgrade.sh could check if kimaki.service has User=root and refuse to proceed (or warn loudly) without an explicit override. Operators upgrading a previously-installed instance should be nudged to migrate.
D. Document the trap in setup docs
Currently the install docs imply RUN_AS_ROOT is a benign "do you have root?" toggle. They should make clear that choosing it is a long-term commitment to manually managing file ownership across the WP install.
Acceptance
- New installs default to
SERVICE_USER=opencode and don't accept RUN_AS_ROOT=true without an explicit, documented opt-in.
- Existing installs running as root can migrate in-place without reinstalling everything.
- Documentation makes the tradeoff explicit before the operator chooses.
Related
This combines with data-machine-code composing AGENTS.md under root → baking --allow-root into every wp-cli example → coding agents inheriting the pattern. Fixing this issue at the wp-coding-agents layer would let DMC's posix_geteuid() check return non-zero, which would naturally drop --allow-root from AGENTS.md examples without any DMC code change.
Problem
When the install/upgrade flow runs with
RUN_AS_ROOT=true(the elif branch inlib/detect.sh), the resultingkimaki.servicesystemd unit getsUser=root. This is the entry point for a long chain of root-ownership inheritance that breaks WordPress auto-update over time.The cascade
kimaki.serviceruns as root → opencode workers spawned by kimaki run as root →wpinvocations run as root → every file PHP/WP-CLI writes is ownedroot:rootmode0644.data-machine-codecomposesAGENTS.mdunder root → itsdatamachine_code_resolve_wp_cli_cmd()correctly detectsposix_geteuid() === 0and bakeswp --allow-rootinto every example. Coding agents then dutifully use--allow-rootfor every command, which keeps the root-write loop going.WP-CLI core upgrade (
wp core update) run by an agent as root creates new files inwp-admin/,wp-includes/, and bundled plugin/theme dirs asroot:root. The next time WordPress tries to auto-update those bundled plugins (akismet, etc.) via the PHP-FPMwww-datauser, it fails withcopy_failed_for_update_core_file/copy_failed_copy_dir_pluginsbecause www-data can't overwrite root-owned files.DM image processing, OG card generation, daily memory writes, agent file scaffolding — all create
root:rootfiles insidewp-content/uploads/because they run inside the same root-process tree.Real-world impact (extrachill.com production, May 1-2 2026)
wp --allow-root core update, ~6,000 root-owned files landed acrosswp-admin/,wp-includes/, and bundled plugin dirs (akismet, breeze, easy-wp-smtp, gutenberg, plugin-check, twentytwentyX themes).copy_failed_*errors before the underlying ownership/permission state was caught and fixed manually.wp-content/uploads/over previous coding-agent sessions, contributing background bloat.Why the non-root path exists but isn't used
lib/detect.shlines 164-180 define three modes:The default branch correctly creates the
opencodesystem user (added towww-datagroup inlib/infrastructure.sh), chownsKIMAKI_DATA_DIRto it, and templatesUser=opencodeinto all systemd units. This is exactly the right architecture.The problem is
RUN_AS_ROOT=trueis too easy to enable, and once enabled, every downstream artifact (systemd unit, file ownership, AGENTS.md instructions) bakes in the root assumption. There's no recovery path short of full reinstall + re-chown.Proposed fixes (any subset)
A. Deprecate
RUN_AS_ROOT=trueor hide it behind an explicit flagThe non-root path works correctly and is already the default. The root path exists for what use case? If it's "user can't be bothered to create a system user," that's a setup convenience that creates long-term operational damage. Consider:
RUN_AS_ROOTentirelyKNOWN_BAD_RUN_AS_ROOT=truethat signals the operator is choosing painB. Add a
kimaki doctor/repairsubcommandFor installations already in the
RUN_AS_ROOT=truestate, provide an in-place migration:That would:
opencodeuser if missingKIMAKI_DATA_DIR,DM_WORKSPACE_DIR, and the WordPress install (or at leastwp-admin/,wp-includes/, and bundled plugins) to the new userUser=directivedaemon-reloadandrestart kimakiC. Detect the bad state at upgrade time
upgrade.shcould check ifkimaki.servicehasUser=rootand refuse to proceed (or warn loudly) without an explicit override. Operators upgrading a previously-installed instance should be nudged to migrate.D. Document the trap in setup docs
Currently the install docs imply
RUN_AS_ROOTis a benign "do you have root?" toggle. They should make clear that choosing it is a long-term commitment to manually managing file ownership across the WP install.Acceptance
SERVICE_USER=opencodeand don't acceptRUN_AS_ROOT=truewithout an explicit, documented opt-in.Related
This combines with
data-machine-codecomposingAGENTS.mdunder root → baking--allow-rootinto every wp-cli example → coding agents inheriting the pattern. Fixing this issue at thewp-coding-agentslayer would let DMC'sposix_geteuid()check return non-zero, which would naturally drop--allow-rootfrom AGENTS.md examples without any DMC code change.