Skip to content

RUN_AS_ROOT path leads to root-owned files cascading through site (cause: kimaki.service runs as root, every wp-cli inherits root, every file written is root-owned) #93

@chubes4

Description

@chubes4

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

  1. 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.

  2. 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.

  3. 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.

  4. 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:

  1. Create the opencode user if missing
  2. 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
  3. Update systemd unit User= directive
  4. Fix file mode on root-owned WP files (chmod g+w so www-data can still update them during transition)
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions