build-modify
Session-aware per-step execution engine for /modify - surgical theme edits, WP-CLI content/plugin changes, lazy Docker startup, and versioned output on session completion
No fetch-and-execute, destructive, credential, exfiltration or rules-override patterns in 1 files, checked on Oct 20, 2026. This is not a guarantee of safety. How the scan works.
npx degit Shaisolaris/solaris-dev-shop/employees/specialized/wordpress-master/diagnostics/build-modify .claude/skills/build-modify12 alternatives · Best design skills · Author? Prove this skill
SKILL.md
# Build Modify Skill
Session-aware execution engine for the `/modify` command. COMMAND.md owns the interactive session loop and invokes this skill's sections per step. Each atomic modification step produces its own git commit and build.json entry. No zip or SETUP.md regeneration occurs per step - those are deferred to session completion (Section 7).
This skill expects the following variables to be set by the calling command (`/modify`) before invocation:
| Variable | Source | Description |
|---|---|---|
| `WP_DIR` | /modify argument | Path to WordPress directory (set once at session start) |
| `MODIFY_MODE` | /modify session loop | `"nl"` for natural language, `"visual"` for design re-export (set per step) |
| `NL_REQUEST` | /modify session loop | Natural language modification request (set per NL step) |
| `VISUAL_PATH` | /modify session loop | Path to updated design export directory (set per visual step) |
---
## Section 0: Prerequisites and Setup
Set up lazy Docker/WP-CLI helpers, declare the `git_commit_stage` helper function, and register an EXIT trap for MySQL container cleanup. Docker is NOT checked upfront - it is started lazily only when a content or plugin step requires MySQL.
```bash
# ── Lazy Docker/WP-CLI helpers ───────────────────────────────────────────────
# Docker and WP-CLI are only needed for content and plugin steps.
# Theme-only sessions (theme-token, template-edit) work without Docker entirely.
WP_CLI_MODE=""
WP=""
DOCKER_CHECKED=false
ensure_wp_cli() {
if [ "$DOCKER_CHECKED" = "true" ]; then
return 0
fi
# Check Docker is running
if ! docker info > /dev/null 2>&1; then
echo "[Modify] ERROR: Docker is required for content and plugin modifications."
echo "[Modify] Start Docker Desktop and try again."
echo "[Modify] Theme-only modifications (theme-token, template-edit) work without Docker."
return 1
fi
# Check for local WP-CLI first (preferred), Docker fallback
if which wp > /dev/null 2>&1; then
WP_CLI_MODE="local"
WP="wp --path=$BUILD_DIR"
echo "[Modify] WP-CLI ready (local)"
else
if docker run --rm wordpress:cli wp --version > /dev/null 2>&1; then
WP_CLI_MODE="docker"
WP="docker run --rm -v \"$BUILD_DIR:/var/www/html\" --network host wordpress:cli wp --allow-root"
echo "[Modify] WP-CLI ready (docker)"
else
echo "[Modify] ERROR: WP-CLI is required for content/plugin modifications but was not found."
return 1
fi
fi
DOCKER_CHECKED=true
return 0
}
ensure_docker_mysql() {
# Ensure WP-CLI is available first
if ! ensure_wp_cli; then
STEP_SKIPPED=true
return 1
fi
# Check database.sql exists
if [ ! -f "$BUILD_DIR/database.sql" ]; then
echo "[Modify] ERROR: database.sql not found in build directory."
echo "[Modify] Cannot perform content modifications without a database."
echo "[Modify] Theme-only modifications (theme-token, template-edit) are still possible."
STEP_SKIPPED=true
return 1
fi
# If MySQL is already running, reuse it
if [ "$MYSQL_RUNNING" = "true" ]; then
return 0
fi
# Read existing credentials from wp-config.php
EXISTING_DB_PASS=$($WP config get DB_PASSWORD 2>/dev/null || echo "")
EXISTING_DB_USER=$($WP config get DB_USER 2>/dev/null || echo "wp")
if [ -z "$EXISTING_DB_PASS" ]; then
EXISTING_DB_PASS="$(openssl rand -hex 12)"
EXISTING_DB_USER="wp"
echo "[Modify] NOTE: Could not read existing DB credentials - using generated credentials"
fi
# Port 3307 conflict check
if lsof -i :3307 > /dev/null 2>&1; then
echo "[Modify] ERROR: Port 3307 is already in use. Close the conflicting process and try again."
STEP_SKIPPED=true
return 1
fi
MYSQL_ROOT_PASS="wpmod_root_$(openssl rand -hex 8)"
# Start MySQL container with credentials matching wp-config.php
docker run -d \
--name "$MYSQL_CONTAINER" \
-e MYSQL_ROOT_PASSWORD="$MYSQL_ROOT_PASS" \
-e MYSQL_DATABASE=wordpress \
-e MYSQL_USER="$EXISTING_DB_USER" \
-e MYSQL_PASSWORD="$EXISTING_DB_PASS" \
-p 127.0.0.1:3307:3306 \
mysql:8.0 \
--default-authentication-plugin=mysql_native_password \
> /dev/null
MYSQL_RUNNING=true
echo "[Modify] Starting MySQL container..."
# Wait for ready (up to 30 seconds)
WAIT_COUNT=0
until docker exec "$MYSQL_CONTAINER" mysqladmin ping --silent 2>/dev/null; do
WAIT_COUNT=$((WAIT_COUNT + 1))
if [ "$WAIT_COUNT" -ge 30 ]; then
echo "[Modify] ERROR: MySQL container failed to start within 30 seconds."
STEP_SKIPPED=true
return 1
fi
sleep 1
done
echo "[Modify] Database ready."
# Import existing database.sql
if ! $WP db import "$BUILD_DIR/database.sql" 2>&1; then
echo "[Modify] ERROR: Database import failed. Cannot proceed with content modification."
STEP_SKIPPED=true
return 1
fi
echo "[Modify] Database imported into ephemeral container."
return 0
}
# ── MySQL container EXIT trap ────────────────────────────────────────────────
MYSQL_CONTAINER="wpmodify-mysql-$$"
MYSQL_RUNNING=false
cleanup_modify_mysql() {
if [ "$MYSQL_RUNNING" = "true" ]; then
echo "[Modify] Cleaning up Docker MySQL container..."
docker rm -f "$MYSQL_CONTAINER" 2>/dev/null || true
echo "[Modify] MySQL cleanup complete."
fi
}
trap cleanup_modify_mysql EXIT
# ── git_commit_stage function ────────────────────────────────────────────────
# Redeclared from build-git Section 4 - skills are separate execution contexts.
# Usage: git_commit_stage "commit subject" ["commit body"]
# Failures are warn-and-continue - git failures never abort the modification.
git_commit_stage() {
local SUBJECT="$1"
local BODY="${2:-}"
if [ "${GIT_INITIALIZED:-true}" != "true" ]; then
echo "[Modify] WARNING: Git not initialized - skipping commit: $SUBJECT"
return 0
fi
echo "[Modify] Git commit: $SUBJECT"
# Stage all changes
if ! git -C "$BUILD_DIR" add -A > /dev/null 2>&1; then
echo "[Modify] WARNING: git add failed for commit: $SUBJECT"
return 0
fi
# Check if there is anything to commit
if git -C "$BUILD_DIR" diff --cached --quiet 2>/dev/null; then
echo "[Modify] NOTE: Nothing to commit for: $SUBJECT (no changes staged)"
return 0
fi
# Commit - use -m twice for multiline commits (subject + body)
if [ -n "$BODY" ]; then
if git -C "$BUILD_DIR" commit -m "$SUBJECT" -m "$BODY" > /dev/null 2>&1; then
echo "[Modify] Committed: $SUBJECT"
else
echo "[Modify] WARNING: git commit failed for: $SUBJECT"
fi
else
if git -C "$BUILD_DIR" commit -m "$SUBJECT" > /dev/null 2>&1; then
echo "[Modify] Committed: $SUBJECT"
else
echo "[Modify] WARNING: git commit failed for: $SUBJECT"
fi
fi
echo ""
}
```
## Section 1: WordPress Directory Validation and State Loading
Use the WordPress directory path (WP_DIR) directly. Validate wp-content/ exists, load build.json if present, or create a minimal one for external WordPress directories. This section runs once at session start.
```bash
BUILD_DIR="$WP_DIR"
echo "[Modify] WordPress directory: $BUILD_DIR"
echo ""
# ── Validate wp-content/ exists (belt-and-suspenders - COMMAND.md already checked) ──
if [ ! -d "$BUILD_DIR/wp-content" ]; then
echo "ERROR: wp-content/ not found in $BUILD_DIR"
echo "This does not appear to be a WordPress installation."
exit 1
fi
# ── Load or create build.json ────────────────────────────────────────────────
BUILD_JSON_PATH="$BUILD_DIR/build.json"
if [ -f "$BUILD_JSON_PATH" ]; then
echo "[Modify] Loading state from build.json..."
STATE_JSON=$(python3 << 'PYEOF'
import json, os, sys
build_json_path = os.path.join(os.environ.get("BUILD_DIR", ""), "build.json")
try:
with open(build_json_path) as f:
state = json.load(f)
print(json.dumps(state))
except Exception:
print("{}")
PYEOF
)
# Extract key state values
THEME_SLUG=$(echo "$STATE_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('theme',{}).get('slug',''))" 2>/dev/null)
CREATION_MODE=$(echo "$STATE_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('creation_mode', d.get('mode','')))" 2>/dev/null)
EXISTING_MODS=$(echo "$STATE_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('modifications',[])))" 2>/dev/null)
else
echo "[Modify] No build.json found - this appears to be an external WordPress directory."
echo "[Modify] Creating build.json to track modifications."
# Auto-detect theme slug: first non-twentytwenty* theme in wp-content/themes/
THEME_SLUG=$(python3 << PYEOF
import os, sys
themes_dir = os.path.join("$BUILD_DIR", "wp-content", "themes")
if not os.path.isdir(themes_dir):
print("")
sys.exit(0)
themes = sorted(os.listdir(themes_dir))
# Prefer non-core themes
for t in themes:
if os.path.isdir(os.path.join(themes_dir, t)) and not t.startswith("twentytwenty"):
print(t)
sys.exit(0)
# Fallback to first theme
for t in themes:
if os.path.isdir(os.path.join(themes_dir, t)):
print(t)
sys.exit(0)
print("")
PYEOF
)
CREATION_MODE="external"
EXISTING_MODS=0
# Create minimal build.json
python3 << PYEOF
import json, os
from datetime import datetime, timezone
build_dir = "$BUILD_DIR"
theme_slug = "$THEME_SLUG"
build_id = os.path.basename(os.path.abspath(build_dir))
state = {
"build_id": build_id,
"creation_mode": "external",
"created_at": datetime.now(timezone.utc).isoformat(),
"build_dir": os.path.abspath(build_dir),
"theme": {"slug": theme_slug} if theme_slug else {},
"modifications": []
}
build_json_path = os.path.join(build_dir, "build.json")
with open(build_json_path, "w") as f:
json.dump(state, f, indent=2)
f.write("\n")
print(f"[Modify] Created build.json (build_id: {build_id}, theme: {theme_slug or 'unknown'})")
PYEOF
fi
# Derive BUILD_ID from build.json for downstream sections (commit messages, output naming)
BUILD_ID=$(python3 -c "import json; print(json.load(open('$BUILD_DIR/build.json')).get('build_id',''))" 2>/dev/null)
echo "[Modify] Theme: ${THEME_SLUG:-unknown}"
echo "[Modify] Creation mode: ${CREATION_MODE:-unknown}"
echo "[Modify] Existing modifications: ${EXISTING_MODS:-0}"
# ── Compute next version ─────────────────────────────────────────────────────
# Version starts at 1 for the first modification
NEXT_VERSION=$((${EXISTING_MODS:-0} + 1))
echo "[Modify] Next version: v${NEXT_VERSION}"
echo ""
# ── Detect git repo ──────────────────────────────────────────────────────────
if git -C "$BUILD_DIR" rev-parse --git-dir > /dev/null 2>&1; then
echo "[Modify] Git repo detected - appending modification commits"
GIT_INITIALIZED=true
else
echo "[Modify] No git repo found - initializing for modification tracking"
# Initialize git with the same pattern as build-git Section 1
if git -C "$BUILD_DIR" init -b main > /dev/null 2>&1; then
# Set fallback git identity if needed
GIT_USER_NAME=$(git config --global user.name 2>/dev/null || echo "")
GIT_USER_EMAIL=$(git config --global user.email 2>/dev/null || echo "")
if [ -z "$GIT_USER_NAME" ] || [ -z "$GIT_USER_EMAIL" ]; then
git -C "$BUILD_DIR" config user.name "CoWork Build"
git -C "$BUILD_DIR" config user.email "build@cowork.local"
fi
# Write .gitignore from build-git Section 2 pattern
cat > "$BUILD_DIR/.gitignore" << 'GITIGNORE'
# WordPress Build - Generated .gitignore
# Tracks: custom-* prefixed themes and plugins, README.md, SETUP.md, build.json
# Excludes: WP core, ephemeral dirs, database, MCP adapter, WP.org-sourced components
# ── WordPress Core Root Files ──────────────────────────────────────────────
index.php
license.txt
readme.html
wp-activate.php
wp-blog-header.php
wp-comments-post.php
wp-config.php
wp-config-sample.php
wp-cron.php
wp-links-opml.php
wp-load.php
wp-login.php
wp-mail.php
wp-settings.php
wp-signup.php
wp-trackback.php
xmlrpc.php
# ── WordPress Core Directories ─────────────────────────────────────────────
wp-admin/
wp-includes/
# ── Ephemeral wp-content Directories ──────────────────────────────────────
wp-content/cache/
wp-content/upgrade/
wp-content/uploads/
wp-content/backups/
wp-content/w3tc-config/
wp-content/wflogs/
wp-content/updraft/
# ── Database and Debug Logs ────────────────────────────────────────────────
database.sql
wp-content/debug.log
# ── MCP Adapter (bundled, reinstallable from CoWork plugin) ───────────────
wp-content/plugins/mcp-adapter/
# ── WordPress Core Bundled Themes (reinstallable) ─────────────────────────
wp-content/themes/twentytwentyone/
wp-content/themes/twentytwentytwo/
wp-content/themes/twentytwentythree/
wp-content/themes/twentytwentyfour/
wp-content/themes/twentytwentyfive/
GITIGNORE
# Create initial commit capturing current state
git -C "$BUILD_DIR" add -A > /dev/null 2>&1
git -C "$BUILD_DIR" commit -m "build(init): capture existing state for modification tracking" > /dev/null 2>&1
GIT_INITIALIZED=true
echo "[Modify] Git initialized with initial state commit"
else
GIT_INITIALIZED=false
echo "[Modify] WARNING: git init failed - modifications will proceed without git history"
fi
fi
echo ""
```
## Section 2: NL Request Decomposition (NL mode only)
the coding agent reads the NL_REQUEST and decomposes it into a sequence of atomic steps. Each step has a type from the modification taxonomy. Compound requests (e.g. "change the color and add a blog page") are split into separate steps, each getting its own git commit and build.json entry.
**Skip this section entirely for visual mode** - visual modifications are handled as a single atomic step in Section 5.
```
Decomposition is a the coding agent in-context reasoning step, not automated parsing.
the coding agent reads the NL_REQUEST and the current build state (theme slug, installed plugins,
existing content from build.json) to produce an ordered list of atomic steps.
Step Type Taxonomy:
┌──────────────────┬─────────────────────────────────────────────────────────────┐
│ Step Type │ Description │
├──────────────────┼─────────────────────────────────────────────────────────────┤
│ theme-token │ Change a value in theme.json (color, font, spacing) │
│ template-edit │ Edit a specific .html template or part file │
│ content-edit │ Modify an existing page or post via WP-CLI (requires MySQL) │
│ content-create │ Create a new page, post, or menu item (requires MySQL) │
│ plugin-add │ Install and activate a WP.org plugin (requires MySQL) │
│ plugin-remove │ Deactivate and uninstall a plugin (requires MySQL) │
└──────────────────┴─────────────────────────────────────────────────────────────┘
Output: an ordered list of atomic steps with type, target, and specific change description.
Example decomposition:
User request: "change the primary color to forest green and add a blog page"
Step 1: [theme-token] theme.json → settings.color.palette → primary → #2d5a27
Step 2: [content-create] Create new page "Blog" with wp post create, link in navigation menu
Each step produces:
- One git commit: build(modify): "change the primary color to forest green and add a blog page" (step 1/2)
- One build.json modifications entry
For a single-step request, omit the "(step N/M)" suffix from the commit message.
IMPORTANT: Content steps (content-edit, content-create, plugin-add, plugin-remove) all
require MySQL. These steps call ensure_docker_mysql() which handles lazy Docker startup.
If Docker is unavailable, content/plugin steps are skipped with a warning - theme-only
steps still succeed.
```
## Section 3: NL Modification Execution
Execute each atomic step from the decomposition. Each sub-section handles a different step type. The per-step finalization (Section 4) runs after each step completes.
### Section 3a: theme-token Steps
Read `theme.json` with Python `json.load`, locate the target token, apply the change, validate, and write back. Never use sed/awk for JSON editing. No Docker required.
```bash
# theme-token modification pattern
# Variables: THEME_SLUG, TARGET_SLUG (e.g. "primary"), NEW_VALUE (e.g. "#2d5a27"), TOKEN_PATH (e.g. "color.palette")
THEME_JSON_PATH="$BUILD_DIR/wp-content/themes/$THEME_SLUG/theme.json"
if [ ! -f "$THEME_JSON_PATH" ]; then
echo "[Modify] WARNING: theme.json not found at $THEME_JSON_PATH - skipping theme-token step"
# warn-and-continue
else
# Read, modify, validate, write - all in Python for JSON safety
python3 << PYEOF
import json, sys, os, shutil
theme_json_path = "$THEME_JSON_PATH"
target_slug = "$TARGET_SLUG"
new_value = "$NEW_VALUE"
token_path = "$TOKEN_PATH"
# Read current theme.json
with open(theme_json_path) as f:
theme = json.load(f)
# Back up original for rollback on validation failure
backup_path = theme_json_path + ".bak"
shutil.copy2(theme_json_path, backup_path)
old_value = None
modified = False
# Route by token path type
if token_path.startswith("color.palette"):
palette = theme.get("settings", {}).get("color", {}).get("palette", [])
for entry in palette:
if entry.get("slug") == target_slug:
old_value = entry.get("color")
entry["color"] = new_value
modified = True
break
elif token_path.startswith("typography.fontFamilies"):
font_families = theme.get("settings", {}).get("typography", {}).get("fontFamilies", [])
for entry in font_families:
if entry.get("slug") == target_slug:
old_value = entry.get("fontFamily")
entry["fontFamily"] = new_value
modified = True
break
elif token_path.startswith("spacing"):
spacing = theme.get("settings", {}).get("spacing", {})
if target_slug in spacing:
old_value = spacing[target_slug]
spacing[target_slug] = new_value
modified = True
if not modified:
print(f"[Modify] WARNING: Token '{target_slug}' not found in {token_path} - skipping")
os.remove(backup_path)
sys.exit(0)
# Validate JSON is still valid
try:
json.dumps(theme)
except Exception as e:
print(f"[Modify] ERROR: JSON validation failed after edit - restoring backup")
shutil.move(backup_path, theme_json_path)
sys.exit(1)
# Write updated theme.json
with open(theme_json_path, "w") as f:
json.dump(theme, f, indent=2)
f.write("\n")
# Verify written file is valid JSON
try:
with open(theme_json_path) as f:
json.load(f)
except Exception:
print(f"[Modify] ERROR: Written file is not valid JSON - restoring backup")
shutil.move(backup_path, theme_json_path)
sys.exit(1)
# Clean up backup
os.remove(backup_path)
print(f"[Modify] theme.json updated: {target_slug} {old_value} → {new_value}")
PYEOF
STEP_FILES_CHANGED="wp-content/themes/$THEME_SLUG/theme.json"
STEP_TYPE="theme-token"
STEP_DESCRIPTION="Changed ${target_slug} from ${old_value} to ${new_value}"
BEFORE_JSON="{\"${target_slug}\": \"${old_value}\"}"
AFTER_JSON="{\"${target_slug}\": \"${new_value}\"}"
fi
```
### Section 3b: template-edit Steps
Read the target `.html` template file, apply the the coding agent-interpreted change to the specific block comment section, and write back. Preserve all other blocks verbatim - surgical edit, not full rewrite. No Docker required.
```
Template editing is a the coding agent in-context judgment step.
the coding agent reads the current content of the target template file (from templates/ or parts/)
and the user's modification request. the coding agent then:
1. Identifies which block comment section(s) need to change
2. Rewrites only the affected block(s) using WordPress block comment syntax (<!-- wp:* -->)
3. Preserves all other blocks verbatim
4. Writes the updated template file
The template file is at:
$BUILD_DIR/wp-content/themes/$THEME_SLUG/templates/{template-name}.html
OR
$BUILD_DIR/wp-content/thShown under the source repo’s MIT license. Read the full folder on GitHub.