From wordpress-expert
Connects to a WordPress site via SSH, local directory, Docker container, or git repo; detects installation, verifies WP-CLI, acquires files, and saves profile.
npx claudepluginhub dr-robert-li/cowork-wordpress-expertconnect/# Connect Command Establish a connection to a WordPress site from any source — SSH server, local directory, Docker container, or git repository — detect WordPress installation, verify WP-CLI availability, acquire files, and save a connection profile. ## Command Flow ### 0. Source Type Detection This section runs first, before any source-specific logic. **If /connect is called with no arguments**, show a source type menu: Wait for user input. If user types a number (1-4), route to the corresponding flow. If user types a target directly, run auto-detection below on that input. **If a...
/COMMANDSurfaces and validates Claude's hidden assumptions about project context, tech stack, and standards interactively. Supports --list, --check, --graph modes.
/COMMANDGenerates complete skill packages via co-evolutionary generate-verify-refine loops from task domains, arXiv papers/URLs/PDFs, or existing skills. Supports --budget N, --distill, --dry-run flags.
/COMMANDScans codebase path or scope for vulnerabilities in hardcoded secrets, input validation, auth/authz, dependencies, HTTP headers, and misconfigs. Produces severity-ranked findings with remediation guidance.
/COMMANDGuides through TDD workflow for functions/modules/features: understand requirements, write failing tests (RED), implement minimally (GREEN), refactor, repeat.
/COMMANDRoutes your task to the best armory skills, agents, or commands using a decision tree by dev lifecycle phases (define, plan, build, verify, review, ship) and domains.
/COMMANDSimplifies recently modified files via git diff: analyzes cyclomatic complexity, detects duplicates/nesting, reduces conditionals, extracts helpers, verifies tests.
Establish a connection to a WordPress site from any source — SSH server, local directory, Docker container, or git repository — detect WordPress installation, verify WP-CLI availability, acquire files, and save a connection profile.
This section runs first, before any source-specific logic.
If /connect is called with no arguments, show a source type menu:
What would you like to connect to?
1) SSH — remote server via SSH
2) Local — local WordPress directory
3) Docker — WordPress in a Docker container
4) Git — clone or point to a git repository
Type the number or enter your target directly:
Wait for user input. If user types a number (1-4), route to the corresponding flow. If user types a target directly, run auto-detection below on that input.
If an argument is provided, auto-detect source type using these rules in order:
detect_source_type() {
local input="$1"
# Git URL patterns — must check before SSH user@host
if echo "$input" | grep -qE "^(https?://|git@|git://)"; then
echo "git"
return
fi
# Local path patterns — starts with /, ./, ../, or ~
if echo "$input" | grep -qE "^[./]|^~"; then
echo "local"
return
fi
# SSH: user@host pattern (contains @ and no path separator after host)
if echo "$input" | grep -qE "^[a-zA-Z0-9_-]+@[a-zA-Z0-9._-]+$"; then
echo "ssh"
return
fi
# Ambiguous: bare alphanumeric token (SSH alias or Docker container name)
echo "ambiguous"
}
Detection rules:
https://, git@, or git:// → source type git/, ./, ../, or ~ → source type local (but also check for .git/ subdirectory — if found, ask: "This looks like a git repository. Connect as Git type (enables branch switching)? Or connect as Local type?")user@host pattern (contains @ and no / path separator after the host) → source type sshAfter source type is determined, route to the appropriate flow:
If user provides a site name argument that matches an existing profile in sites.json:
sites.json and look up the site by namegit -C "$local_path" pull origin "$git_branch". If no: "Using existing local files." Update last_sync timestamp. Jump to capability summary display.docker inspect --format='{{.State.Status}}' "$container" 2>/dev/null. If not "running", warn: "Container '[name]' is not running. Start it first, then try again." On reconnect with docker cp source: Re-copy files before diagnostics (same behavior as SSH rsync). Show "Syncing from container..." message.When source type is local:
Resolve path:
wp_path="${input_path/#\~/$HOME}"
wp_path=$(realpath "$wp_path" 2>/dev/null || echo "$wp_path")
Always store the resolved absolute path. Symlinks are resolved — use resolved path consistently throughout.
Check for git repository (before WordPress validation):
if [ -d "$wp_path/.git" ]; then
echo "This looks like a git repository. Connect as:"
echo " 1) Git type — enables branch switching and pull on reconnect"
echo " 2) Local type — treat as a regular local directory"
# Wait for user choice
fi
If user selects Git type, route to Section 1C (existing checkout sub-flow) with this path.
Validate WordPress markers (check all four, warn on partial):
markers_found=0
test -f "$wp_path/wp-config.php" && markers_found=$((markers_found + 1))
test -d "$wp_path/wp-includes/" && markers_found=$((markers_found + 1))
test -d "$wp_path/wp-admin/" && markers_found=$((markers_found + 1))
test -f "$wp_path/wp-load.php" && markers_found=$((markers_found + 1))
Set file paths: For local source, local_path = wp_path (no file copying needed). Files are read directly from this location.
Generate profile name from directory basename:
profile_name=$(basename "$wp_path" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-')
Display suggested name: "Profile name: [name]. Press Enter to accept or type a new name:"
Probe for local WP-CLI:
wp_cli_path=$(which wp 2>/dev/null || echo "null")
If WP-CLI found and wp-config.php present, try to read WordPress info:
wp_version=$(wp core version --path="$wp_path" 2>/dev/null || echo "null")
site_url=$(wp option get siteurl --path="$wp_path" 2>/dev/null || echo "null")
(DB commands will fail gracefully if the database is not accessible — that's OK, continue)
Save profile with source_type "local" (see Section 9, Profile Save Logic).
Show capability summary (see Section 10).
When source type is docker:
If no container name/ID specified, list running containers:
docker ps --format " {{.Names}} ({{.Image}})" 2>/dev/null
Ask: "Enter container name or ID:"
Verify container is running:
container_status=$(docker inspect --format='{{.State.Status}}' "$container" 2>/dev/null)
If status is not "running": "Container '[name]' is not running (status: [status]). Start it first, then try again." → abort.
Probe for WordPress in known paths, in order:
/var/www/html/app/public/var/www/var/www/wordpress/usr/share/nginx/html/srv/wwwFor each path:
docker exec "$container" test -f "$path/wp-config.php" 2>/dev/null
Use the first path that succeeds. If none found: "WordPress not found in standard paths inside container. Enter the WordPress path inside the container:" — wait for user input.
Detect bind mounts covering the WordPress path:
bind_source=$(docker inspect --format='{{json .Mounts}}' "$container" 2>/dev/null | \
jq -r --arg path "$container_wp_path" \
'.[] | select(.Type == "bind") | select($path | startswith(.Destination)) | .Source' | \
head -1)
Important (Pitfall 1 — partial bind mounts): Also check the reverse direction. If only a subdirectory of the WP path is bind-mounted (e.g., /var/www/html/wp-content only), the above filter will not match. Check both directions:
# Direction 1: bind Destination is a prefix of WP path (full WP root is mounted)
bind_source=$(docker inspect --format='{{json .Mounts}}' "$container" | \
jq -r --arg path "$container_wp_path" \
'.[] | select(.Type == "bind") | select($path | startswith(.Destination)) | .Source' | head -1)
# Direction 2 (if no match): bind Destination is a subdirectory of WP path (partial mount)
if [ -z "$bind_source" ]; then
partial_mount=$(docker inspect --format='{{json .Mounts}}' "$container" | \
jq -r --arg path "$container_wp_path" \
'.[] | select(.Type == "bind") | select(.Destination | startswith($path)) | .Destination' | head -1)
if [ -n "$partial_mount" ]; then
echo "Note: Only a subdirectory is bind-mounted ($partial_mount). Falling back to docker cp for full WP root."
fi
fi
If full bind mount found (Direction 1):
local_path = bind mount Source path on hostfile_access = "bind_mount"If no full bind mount (Direction 1 failed, whether or not partial mount exists):
echo "$container" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-'local_path = ".sites/[slug]/"mkdir -p "$local_path"docker cp "${container}:${container_wp_path}/." "$local_path"file_access = "docker_cp"Probe WP-CLI inside container:
wp_cli_path=$(docker exec "$container" which wp 2>/dev/null || \
docker exec "$container" wp --version 2>/dev/null && echo "wp" || echo "null")
If WP-CLI found inside container, read WordPress info via docker exec:
wp_version=$(docker exec "$container" wp core version --path="$container_wp_path" 2>/dev/null || echo "null")
site_url=$(docker exec "$container" wp option get siteurl --path="$container_wp_path" 2>/dev/null || echo "null")
Generate profile name from container name (lowercase, hyphenated). Display and ask user to confirm:
profile_name=$(echo "$container" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-')
"Profile name: [name]. Press Enter to accept or type a new name:"
Save profile with source_type "docker", container_name, file_access, and gathered data (see Section 9).
Show capability summary (see Section 10).
When source type is git:
When input is a git URL (https://, git@, git://).
Extract site slug from URL:
site_slug=$(echo "$git_url" | sed 's|.*[:/]||; s|\.git$||' | \
tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-')
List remote branches without cloning:
default_branch=$(git ls-remote --symref "$git_url" HEAD 2>/dev/null | \
grep "^ref:" | sed 's|ref: refs/heads/||; s|\s.*||')
default_branch="${default_branch:-main}"
branch_list=$(git ls-remote --heads "$git_url" 2>/dev/null | sed 's|.*refs/heads/||' | head -10)
branch_count=$(echo "$branch_list" | grep -c .)
If multiple branches exist:
Repository has [N] branches. Using default: [branch].
Other branches: [first 4, comma-separated]
Clone a different branch? (enter name, or press Enter for [default]):
Wait for user input. Use branch_choice if provided, else use default_branch.
Shallow clone:
git clone --depth 1 --branch "$clone_branch" "$git_url" ".sites/$site_slug/" 2>&1
On clone failure:
git@ URLs: "Clone failed. Check that your SSH key is loaded (ssh-add -l) and has access to this repository."https:// URLs: "Clone failed. Check network connectivity and repository access. For private repos, use a personal access token in the URL."rm -rf ".sites/$site_slug/"Set paths: local_path = .sites/$site_slug/, wp_path = same.
Validate WordPress markers (same as local flow — 4-marker check with partial warning): Note: git repos may be theme-only or plugin-only. On partial match, warn: "This repository appears to contain only part of a WordPress installation (e.g., a theme or plugin). File analysis will work; DB skills are not available for git sources."
Probe local WP-CLI:
wp_cli_path=$(which wp 2>/dev/null || echo "null")
Note: "Git sources have no live database. WP-CLI DB commands will not work even if WP-CLI is installed."
Save profile with source_type "git", git_remote = $git_url, git_branch = $clone_branch, file_access = "direct" (see Section 9).
Show capability summary (see Section 10).
When input is a local path AND contains a .git/ subdirectory AND user chose "Git" type (either explicitly from menu, or from the prompt in Section 1A step 2).
Resolve path:
abs_path=$(realpath "$input_path" 2>/dev/null || echo "$input_path")
local_path="$abs_path"
wp_path="$abs_path"
Read git metadata:
git_remote=$(git -C "$abs_path" remote get-url origin 2>/dev/null || echo "none")
git_branch=$(git -C "$abs_path" branch --show-current 2>/dev/null || echo "unknown")
If multiple remote branches exist, mention them and offer to switch:
branch_count=$(git -C "$abs_path" branch -r 2>/dev/null | grep -c .)
if [ "$branch_count" -gt 1 ]; then
echo "Current branch: $git_branch"
echo "Other branches available:"
git -C "$abs_path" branch -r 2>/dev/null | grep -v HEAD | head -5 | sed 's/.*origin\// /'
echo "Switch branch? (enter branch name, or press Enter to keep $git_branch)"
# Wait for user input; if provided, run: git -C "$abs_path" checkout "$branch_choice"
fi
Validate WordPress markers (same 4-marker check with partial warning).
Save profile with source_type "git", git_remote, git_branch, file_access = "direct" (see Section 9).
Show capability summary (see Section 10).
Reconnect behavior for git profiles (called from Section 1, saved profile shortcut):
When /connect is called with a profile name that already exists AND has source_type "git":
test -d "$local_path"git -C "$local_path" pull origin "$git_branch" — show result, warn on failureThis section runs only when source_type is "ssh".
Ask for details one at a time, waiting for user response after each question:
Step 2a: Hostname/IP
ssh -G {hostname} 2>/dev/null | grep "^hostname "
ssh -G {hostname}:
ssh -G {hostname} | grep "^hostname " | awk '{print $2}'
ssh -G {hostname} | grep "^user " | awk '{print $2}'
ssh -G {hostname} | grep "^port " | awk '{print $2}'
ssh -G {hostname} | grep "^identityfile " | awk '{print $2}'
Step 2b: SSH User
Step 2c: SSH Key Path
Step 2d: Remote WordPress Path
This section runs only when source_type is "ssh".
Test SSH connectivity with BatchMode (no password prompts) and timeout:
ssh -o BatchMode=yes \
-o ConnectTimeout=10 \
-o StrictHostKeyChecking=accept-new \
{user}@{host} "echo 'connected'" 2>&1
On success (exit code 0):
On failure (exit code non-zero):
chmod 600 {key_path}"/connect again."This section runs only when source_type is "ssh".
If user provided a path in step 2d:
If path was blank (auto-detect):
COMMON_PATHS=(
"/var/www/html"
"/home/{user}/public_html"
"/usr/share/nginx/html"
"/srv/www"
"~/www"
"~/public_html"
"~/htdocs"
)
ssh {user}@{host} "test -f {path}/wp-config.php" 2>/dev/null
1. /var/www/html
2. /home/user/public_html
Validate WordPress installation:
ssh {user}@{host} "test -f {wp_path}/wp-config.php && \
test -d {wp_path}/wp-content && \
test -d {wp_path}/wp-includes && \
test -f {wp_path}/wp-load.php" 2>/dev/null
/connect again."This section runs only when source_type is "ssh".
Check if WP-CLI is in PATH:
ssh {user}@{host} "which wp" 2>/dev/null
If found in PATH:
If not in PATH, check common locations:
for path in /usr/local/bin/wp /usr/bin/wp ~/bin/wp ~/.local/bin/wp; do
ssh {user}@{host} "test -x $path" 2>/dev/null && echo "$path"
done
If found in common location:
If not found:
ssh {user}@{host} "sudo -n true" 2>/dev/null
ssh {user}@{host} "curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
chmod +x wp-cli.phar && \
sudo mv wp-cli.phar /usr/local/bin/wp"
ssh {user}@{host} "mkdir -p ~/bin && \
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
chmod +x wp-cli.phar && \
mv wp-cli.phar ~/bin/wp"
Version check (if WP-CLI found or installed):
ssh {user}@{host} "{wp_cli_path} --version" 2>/dev/null
This section runs only when source_type is "ssh".
Run these commands over SSH (all with cd {wp_path} && prefix):
# WordPress core version
WP_VERSION=$(ssh {user}@{host} "cd {wp_path} && {wp_cli_path} core version")
# Site URL
SITE_URL=$(ssh {user}@{host} "cd {wp_path} && {wp_cli_path} option get siteurl")
# Plugin summary
PLUGIN_LIST=$(ssh {user}@{host} "cd {wp_path} && {wp_cli_path} plugin list --format=csv --fields=name,status,version")
PLUGIN_COUNT=$(echo "$PLUGIN_LIST" | wc -l)
PLUGIN_COUNT=$((PLUGIN_COUNT - 1)) # Subtract header line
# Active theme
ACTIVE_THEME=$(ssh {user}@{host} "cd {wp_path} && {wp_cli_path} theme list --status=active --field=name")
Display concise summary to user:
WordPress {WP_VERSION} at {SITE_URL}
{PLUGIN_COUNT} plugins installed
Active theme: {ACTIVE_THEME}
Store gathered data (WP_VERSION, SITE_URL, ACTIVE_THEME) for profile saving in step 9.
This section runs only when source_type is "ssh".
Check remote directory size:
REMOTE_SIZE=$(ssh {user}@{host} "du -sb {wp_path} 2>/dev/null | cut -f1")
REMOTE_SIZE_MB=$((REMOTE_SIZE / 1024 / 1024))
Display: "Remote site size: {REMOTE_SIZE_MB}MB"
If size over 500MB:
Detect rsync variant for macOS compatibility:
RSYNC_VERSION=$(rsync --version 2>&1 | head -1)
Create local directory:
mkdir -p .sites/{site-name}/
Execute rsync with exclusions:
# If GNU rsync (version 3.x)
rsync -avz \
--info=progress2 \
--exclude='wp-content/uploads/' \
--exclude='wp-content/cache/' \
--exclude='wp-content/w3tc-cache/' \
--exclude='node_modules/' \
--exclude='vendor/' \
--exclude='.git/' \
--exclude='.env' \
--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r \
{user}@{host}:{wp_path}/ .sites/{site-name}/
# If openrsync (macOS default)
rsync -avz \
-v \
--exclude='wp-content/uploads/' \
--exclude='wp-content/cache/' \
--exclude='wp-content/w3tc-cache/' \
--exclude='node_modules/' \
--exclude='vendor/' \
--exclude='.git/' \
--exclude='.env' \
--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r \
{user}@{host}:{wp_path}/ .sites/{site-name}/
Critical notes:
On sync success:
find .sites/{site-name}/ -type f | wc -lOn sync failure:
/connect {site-name} again."If at any point during the flow user explicitly says "don't save" or "one-off connection":
/connect and provide details again."Generate site/profile name:
Create sites.json if missing:
if [ ! -f sites.json ]; then
echo '{"sites":{}}' > sites.json
fi
Atomic update with jq — includes all source_type fields for every profile:
jq --arg name "$SITE_NAME" \
--arg host "${HOST:-null}" \
--arg user "${SSH_USER:-null}" \
--arg key "${KEY_PATH:-null}" \
--arg wp_path "$WP_PATH" \
--arg local_path "$LOCAL_PATH" \
--arg wp_version "${WP_VERSION:-null}" \
--arg site_url "${SITE_URL:-null}" \
--arg wp_cli "${WP_CLI_PATH:-null}" \
--arg source_type "$SOURCE_TYPE" \
--arg container_name "${CONTAINER_NAME:-null}" \
--arg git_remote "${GIT_REMOTE:-null}" \
--arg git_branch "${GIT_BRANCH:-null}" \
--arg file_access "${FILE_ACCESS:-direct}" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'.sites[$name] = {
"host": (if $host == "null" then null else $host end),
"user": (if $user == "null" then null else $user end),
"ssh_key": (if $key == "null" then null else $key end),
"wp_path": $wp_path,
"local_path": $local_path,
"wp_version": (if $wp_version == "null" then null else $wp_version end),
"site_url": (if $site_url == "null" then null else $site_url end),
"wp_cli_path": (if $wp_cli == "null" then null else $wp_cli end),
"last_sync": $timestamp,
"created_at": (.sites[$name].created_at // $timestamp),
"environment": null,
"is_default": (if (.sites | length) == 0 then true else (.sites[$name].is_default // false) end),
"notes": null,
"source_type": $source_type,
"container_name": (if $container_name == "null" then null else $container_name end),
"git_remote": (if $git_remote == "null" then null else $git_remote end),
"git_branch": (if $git_branch == "null" then null else $git_branch end),
"file_access": $file_access
}' sites.json > /tmp/sites.json.tmp
Source type field values by connection type:
| Source Type | source_type | container_name | git_remote | git_branch | file_access |
|---|---|---|---|---|---|
| SSH | "ssh" | null | null | null | "rsync" |
| Local | "local" | null | null | null | "direct" |
| Docker (bind mount) | "docker" | container name | null | null | "bind_mount" |
| Docker (docker cp) | "docker" | container name | null | null | "docker_cp" |
| Git (fresh clone) | "git" | null | clone URL | branch name | "direct" |
| Git (existing checkout) | "git" | null | origin URL | current branch | "direct" |
Backward compatibility: Existing SSH profiles without source_type are treated as source_type: "ssh" throughout. Always read with null-coalescing:
SOURCE_TYPE=$(jq -r ".sites[\"$SITE_NAME\"].source_type // \"ssh\"" sites.json)
Validate JSON before replacing:
if jq empty /tmp/sites.json.tmp 2>/dev/null; then
mv /tmp/sites.json.tmp sites.json
else
echo "ERROR: Failed to save profile (invalid JSON generated)"
rm -f /tmp/sites.json.tmp
exit 1
fi
If this is the first site saved:
If not the first site:
/connect {site-name} to reconnect."After saving the profile, display which skill categories are available for this source type and WP-CLI status:
Connected: {site-name} [{SOURCE_TYPE_BADGE}]
Available capabilities:
[x] Code quality analysis
[x] Malware scan
[x] WordPress configuration security
[x] Database analysis (WP-CLI available) <- if WP-CLI found
[x] User account audit <- if WP-CLI found
[x] Version audit <- if WP-CLI found
[ ] Database analysis ({reason}) <- if WP-CLI not available
[ ] User account audit ({reason}) <- if WP-CLI not available
[ ] Version audit ({reason}) <- if WP-CLI not available
Source type badge: [SSH], [LOCAL], [DOCKER], or [GIT]
Reasons for WP-CLI unavailability by source type:
For git sources, always note regardless of WP-CLI presence: "Note: Git sources provide file analysis only. No live database connection available."
For docker (docker_cp) sources:
"Note: Files were copied from the container. To refresh, reconnect with /connect {site-name}."
After successful connection, mentally populate the "Currently Connected Site" section in CLAUDE.md with:
This is a mental model update for maintaining context during the session. Do NOT write to CLAUDE.md file.
Every command should:
2>&1 to capture stderr$?Example pattern:
OUTPUT=$(command 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "Command failed: $OUTPUT"
echo "Suggested fix: [specific action based on error]"
exit 1
fi
Source-type-specific error notes:
docker command not found → "Docker is not installed or not in PATH. Install Docker Desktop from https://docker.com/get-started"git command not found → "git is not installed. Install with: brew install git (macOS) or apt install git (Linux)"realpath not found on macOS → Fall back to cd "$input_path" && pwd to resolve absolute pathConnection is successful when:
/connect {site-name} shortcut.source_type // "ssh" null-coalescing