Exec (WP-CLI & shell)
Previewcapability-gated
The exec resource runs commands inside an environment and returns the result as a
streaming job — a typed exit code, split stdout and
stderr, and a live SSE tail. It’s the same WP-CLI you already run over the
SSH gateway, reshaped into a first-class, scoped, idempotent API
primitive that automation and agents can call safely.
the scope split — this is the important part
Section titled “the scope split — this is the important part”There are two ways to run a command, and they are deliberately not the same scope:
| Scope | What it grants | Isolation |
|---|---|---|
| wp.cli:exec | Scoped WP-CLI only — wp <subcommand>, no raw shell. |
In every preset; the safe default for WP automation. |
| exec:raw | Arbitrary shell — bash -c '…'. Keys-to-the-kingdom. |
Excluded from every wildcard and preset. Granted one key at a time, explicitly. |
A * wildcard key can run WP-CLI but cannot run raw shell. exec:raw is the
single most dangerous scope on the platform, so it never rides along with anything
else — you grant it deliberately or not at all. See the security model
for why dangerous scopes are isolated.
endpoints
Section titled “endpoints”run a command
Section titled “run a command”POST …/{envID}/exec
wp.cli:exec or exec:raw
Returns 202 Accepted with a job and a Location header. Send an
Idempotency-Key so a retried command never runs twice.
| Parameter | Type | Required | Description |
|---|---|---|---|
shell |
string | yes | wp-cli (needs wp.cli:exec) or bash (needs exec:raw). |
command |
array | yes | The command and its arguments, e.g. ["plugin", "list", "--status=active"]. |
timeout |
integer | no | Max seconds before the job is killed. Example default 30. |
cwd |
string | no | Working directory for bash shells. Ignored for wp-cli. |
The job’s result carries the typed outcome:
{ "exit_code": 0, "stdout": "woocommerce\nwordpress-seo\n", "stderr": "", "duration_ms": 842}A non-zero exit_code produces a succeeded job whose result.exit_code is
non-zero — the job ran, the command failed. Distinguish that from a failed job,
which means the platform couldn’t run the command at all.
saved bookmarks
Section titled “saved bookmarks”GET · POST …/{envID}/exec/bookmarks
Save a frequently-run command under a name and re-run it by reference, so a remediation runbook lives on the platform rather than scattered across shell history.
{ "name": "flush-and-warm", "shell": "wp-cli", "command": ["cache", "flush"] }Run a saved bookmark with POST …/exec { "bookmark": "flush-and-warm" }.
POST …/{envID}/exec:batch
Run an ordered list of commands in one call — the typed equivalent of a small script.
Returns one job; result reports each step’s exit code and output.
{ "commands": [ { "shell": "wp-cli", "command": ["plugin", "update", "--all"] }, { "shell": "wp-cli", "command": ["cache", "flush"] } ], "stop_on_error": true}worked example — stream WP-CLI output
Section titled “worked example — stream WP-CLI output”Update all plugins on staging and watch the output stream live.
-
Run the command. A
wp.cli:exec-scoped key, an idempotency key, a202back.wp plugin update --all on staging curl -X POST https://api.managed.dev/v1/sites/site_01J7.../environments/env_01J8.../exec \-H "Authorization: Bearer mfk_live_9aF2..." \-H "Forge-Version: 2026-06-23" \-H "Idempotency-Key: a91f-22b7-01de" \-H "Content-Type: application/json" \-d '{ "shell": "wp-cli", "command": ["plugin", "update", "--all"] }'wp plugin update --all job, err := mf.Exec.Run(ctx, forge.ExecParams{SiteID: "site_01J7...",EnvID: "env_01J8...",Shell: forge.ShellWPCLI,Command: []string{"plugin", "update", "--all"},IdempotencyKey: "a91f-22b7-01de",})wp plugin update --all const job = await mf.exec.run({ siteId: "site_01J7...", envId: "env_01J8..." },{ shell: "wp-cli", command: ["plugin", "update", "--all"] },{ idempotencyKey: "a91f-22b7-01de" },);wp plugin update --all mf exec --env env_01J8... -- wp plugin update --all202 response HTTP/1.1 202 AcceptedLocation: /v1/jobs/job_01J9...{"data": {"id": "job_01J9...", "type": "exec.run", "status": "queued","created_at": "2026-06-23T18:10:02.004Z","resource": { "type": "exec", "shell": "wp-cli","site_id": "site_01J7...", "env_id": "env_01J8..." }},"request_id": "req_01J9..."} -
Tail the output. SSE streams
stdout/stderrchunks as they arrive.Live-tail the exec job curl -N https://api.managed.dev/v1/jobs/job_01J9.../stream \-H "Authorization: Bearer mfk_live_9aF2..."event: stdout Plugin ‘woocommerce’ updated to 9.1.2 Plugin ‘wordpress-seo’ updated to 23.5 event: done ✓ exit_code 0 · 2 updated · 1.9s
-
Read the typed result. When the job reaches
succeeded, inspectresult.exit_codeto gate the next step. See async jobs.