Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3cb1430
Fix the gating security findings from the pre-publish scan (1.1.1)
dennisdornon Jul 26, 2026
578241c
review fixes: self-review round (mask idempotency, surrogate-safe cap)
dennisdornon Jul 26, 2026
9623049
review fixes: codex adversarial round 1
dennisdornon Jul 26, 2026
58c7088
review fixes: codex adversarial round 2 + coderabbit iteration 14
dennisdornon Jul 26, 2026
c2836d5
review fixes: coderabbit iteration 15 (CI examples for the mandatory …
dennisdornon Jul 26, 2026
7f92987
review fixes: codex adversarial round 3
dennisdornon Jul 26, 2026
94ad3f5
Stop a URL authority from running through surrounding text
dennisdornon Jul 26, 2026
4263383
review fixes: codex adversarial round 4
dennisdornon Jul 26, 2026
148e4a6
Leave an empty URL userinfo unmasked
dennisdornon Jul 26, 2026
c70da68
review fixes: codex adversarial round 5
dennisdornon Jul 26, 2026
3f15a6c
Mask credentials on internationalized hosts
dennisdornon Jul 26, 2026
805ed89
review fixes: codex adversarial round 6
dennisdornon Jul 26, 2026
1a097b9
review fixes: codex adversarial round 7
dennisdornon Jul 26, 2026
eab67e1
review fixes: codex adversarial round 8
dennisdornon Jul 26, 2026
e46ddde
fix url masking leaks found by differential fuzzing and review round 9
dennisdornon Jul 26, 2026
0b8ce40
type the keytar interop shim instead of casting to any
dennisdornon Jul 26, 2026
3a8abd7
review fixes: codex adversarial round 10
dennisdornon Jul 26, 2026
a84dde3
bound streamed tool calls and classify encoded url parameter keys
dennisdornon Jul 26, 2026
7c9ce6b
classify encoded url parameter keys in error messages too
dennisdornon Jul 26, 2026
623b903
add 1.1.1 changelog entries and document the stream-cap layering
dennisdornon Jul 27, 2026
869bcba
Pin brace-expansion to the patched 5.0.8 in the production tree
dennisdornon Jul 27, 2026
b3716a8
Mask sensitive URL parameters on the debug and doctor paths
dennisdornon Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **Breaking:** `MAINWP_APP_PASSWORD` is now identity-bound the same way keychain credentials are: every authenticated command, `login` included, requires `MAINWP_DASHBOARD_URL` to be set and to match the profile's canonical Dashboard identity before the password is sent. Without it the command refuses to send the credential, with a hint naming the fix. This closes a redirect where an edited or committed `profiles.json` could silently point the environment password at a different host (CI, where the env var is the documented credential path, is exactly where `profiles.json` is easiest to tamper with). Interactive login with a prompted password and display-only commands (`doctor`, `config show`) are unaffected

### Security

- Dashboard URLs carrying credentials in the query string or fragment (`?access_token=...`, `#api_key=...`, including percent-encoded key variants) are rejected when a profile is created; profiles already on disk with such URLs have the sensitive parameter values masked on every display path, including error messages
- Streamed chat tool calls are bounded at every layer (provider stream buffer, engine collection, tool-call envelope), so a hostile or malfunctioning provider stream cannot grow memory or dispatch work without limit
- `abilities info` renders Dashboard-supplied ability descriptions and annotation instructions inside a visibly quoted block, so remote metadata cannot pose as CLI output or smuggle formatting into the terminal

## [1.1.0] - 2026-07-22

### Fixed
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,29 @@ Interactive use needs no configuration beyond `mainwpcontrol login`. For CI, Doc
| Variable | Description |
|----------|-------------|
| `MAINWP_APP_PASSWORD` | Application Password for non-interactive login, and for commands when no OS keychain is available |
| `MAINWP_DASHBOARD_URL` | The Dashboard `MAINWP_APP_PASSWORD` belongs to. Required whenever a command authenticates using that fallback |
| `MAINWPCONTROL_NO_KEYTAR` | Set to `1` to skip keychain loading entirely |
| `MAINWP_ALLOW_HTTP` | Set to `1` to allow insecure `http://` Dashboard URLs |

```bash
export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
export MAINWP_DASHBOARD_URL='https://dashboard.example.com'
mainwpcontrol login --url https://dashboard.example.com --username admin
mainwpcontrol abilities list
```

Whenever the password comes from `MAINWP_APP_PASSWORD`, the CLI sends it only to the
Dashboard named in `MAINWP_DASHBOARD_URL`, and fails instead of sending it anywhere
else. That covers `login` as well as later commands: in CI the password usually lives
in a protected secret store while command arguments do not, so pinning the destination
next to the secret is what stops an edited pipeline from redirecting it. It also means
a `profiles.json` someone else can write cannot point your credential at their server.

Credentials in the OS keychain are bound to their Dashboard the same way and need no
extra variable. `login` only prompts for the password when `MAINWP_APP_PASSWORD` is
unset; when it is set, the same binding applies. Commands that only display
configuration, such as `doctor` and `config show`, are unaffected.

Optional defaults (JSON output, timeouts, chat provider) live in `~/.config/mainwpcontrol/settings.json`. The full list of settings, chat provider keys, and the credential storage model are in the [Configuration guide](docs/configuration.md).

## Abilities
Expand Down Expand Up @@ -212,6 +227,7 @@ CI runs lint, type check, tests, and build on every pull request.
export MAINWP_API_URL=https://your-dashboard.example.com
export MAINWP_USER=your-admin-username
export MAINWP_APP_PASSWORD='your-application-password'
export MAINWP_DASHBOARD_URL="$MAINWP_API_URL"

npm run test:live
```
Expand Down
2 changes: 1 addition & 1 deletion docs/acceptance-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Live credentials are resolved in this order:

The environment file maps `LLM_DASH_URL` to the Dashboard URL and reads `MAINWP_USER` and `MAINWP_APP_PASSWORD`.

The agent runner creates one temporary XDG configuration directory per scenario. Its profile contains the Dashboard URL and username. The Application Password exists only in the Claude child environment as `MAINWP_APP_PASSWORD`; it is not written to the profile, consumer, transcript, command record, or result files. `MAINWPCONTROL_NO_KEYTAR=1` keeps the run independent of the OS keychain.
The agent runner creates one temporary XDG configuration directory per scenario. Its profile contains the Dashboard URL and username. The Application Password exists only in the Claude child environment as `MAINWP_APP_PASSWORD`; it is not written to the profile, consumer, transcript, command record, or result files. The runner also sets `MAINWP_DASHBOARD_URL` to the scenario's Dashboard, which the CLI requires before it will send an environment-supplied password. `MAINWPCONTROL_NO_KEYTAR=1` keeps the run independent of the OS keychain.

`MAINWP_CONTROL_ACCEPTANCE_TOGGLE_PLUGIN` can select the plugin slug preferred by the `agent-plugin-active` scenario and the reversible deterministic plugin scenario.

Expand Down
6 changes: 4 additions & 2 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ Connects to a Dashboard and creates a profile named after its hostname. Interact
# Interactive
mainwpcontrol login

# Non-interactive (CI, headless): password from the environment
# Non-interactive (CI, headless): password from the environment.
# MAINWP_DASHBOARD_URL is required with it and must match --url.
export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
export MAINWP_DASHBOARD_URL='https://dashboard.example.com'
mainwpcontrol login --url https://dashboard.example.com --username admin
```

Expand All @@ -35,7 +37,7 @@ mainwpcontrol login --url https://dashboard.example.com --username admin
| `--password <pw>` | Application Password; prefer `MAINWP_APP_PASSWORD` or the prompt, since flags are visible in the process list |
| `--skip-ssl-verify` | Accept a self-signed certificate for this profile (not for production) |

When no OS keychain is available, credentials are not stored on disk; keep `MAINWP_APP_PASSWORD` set for each run.
When no OS keychain is available, credentials are not stored on disk; keep `MAINWP_APP_PASSWORD` set for each run, along with `MAINWP_DASHBOARD_URL` naming the Dashboard it belongs to. Commands that authenticate release the password only when the two match the profile they are about to contact.

## `abilities list`

Expand Down
14 changes: 13 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,21 @@ For CI, Docker, and machines without a keychain, put the password in the environ

```bash
export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
export MAINWP_DASHBOARD_URL='https://dashboard.example.com'
mainwpcontrol login --url https://dashboard.example.com --username admin
```

When no keychain is available, the password is never written to disk; keep `MAINWP_APP_PASSWORD` set for each run. The profile file is still written and records the Dashboard URL and username, as it does in every mode. If keytar is installed but broken, set `MAINWPCONTROL_NO_KEYTAR=1` to skip loading it.
When no keychain is available, the password is never written to disk; keep both variables set for each run:

```bash
export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
export MAINWP_DASHBOARD_URL='https://dashboard.example.com'
mainwpcontrol abilities list
```

Any command that authenticates with `MAINWP_APP_PASSWORD`, `login` included, sends it only to the Dashboard named in `MAINWP_DASHBOARD_URL` and fails rather than sending it anywhere else. Two things follow: a `profiles.json` that someone else can write cannot point your credential at their server, and in CI, where the password usually comes from a protected secret store and command arguments do not, an edited pipeline cannot redirect it either. Keychain-stored credentials carry the same binding internally and need no extra variable. `login` only prompts for the password when `MAINWP_APP_PASSWORD` is unset; when it is set, the same binding applies. `doctor` and `config show` only display configuration, so they are unaffected.

The profile file is still written and records the Dashboard URL and username, as it does in every mode. If keytar is installed but broken, set `MAINWPCONTROL_NO_KEYTAR=1` to skip loading it.

## Profiles

Expand Down Expand Up @@ -68,6 +79,7 @@ Inspect the active values with `mainwpcontrol config show`.
| Variable | Description |
|----------|-------------|
| `MAINWP_APP_PASSWORD` | Application Password for non-interactive login, and for commands when no keychain is available |
| `MAINWP_DASHBOARD_URL` | The Dashboard `MAINWP_APP_PASSWORD` belongs to. Required whenever a command authenticates using that fallback |
| `MAINWPCONTROL_NO_KEYTAR` | Set to `1` to skip keytar (keychain) loading entirely |
| `MAINWP_ALLOW_HTTP` | Set to `1` to allow insecure HTTP Dashboard URLs |

Expand Down
5 changes: 5 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,16 @@ An environment variable is a named value that programs can read. They're commonl
```bash
# macOS / Linux / Git Bash (lasts until you close the terminal)
export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
export MAINWP_DASHBOARD_URL='https://dashboard.example.com'

# Windows PowerShell (lasts until you close the window)
$env:MAINWP_APP_PASSWORD = 'xxxx xxxx xxxx xxxx xxxx xxxx'
$env:MAINWP_DASHBOARD_URL = 'https://dashboard.example.com'
```

Set both. The second names the Dashboard the password belongs to, and the CLI
refuses to send it anywhere else.

For long-term storage, use the OS keychain (the default when you run `mainwpcontrol login`) or a restricted-permission `.env` file rather than pasting credentials into shell profile files. Note that `mainwpcontrol` does not read `.env` files itself: source the file (or export the variable another way) before running the CLI.

## Reading command output
Expand Down
3 changes: 3 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ Keytar (the keychain module) requires native C++ compilation on some platforms.
```bash
export MAINWPCONTROL_NO_KEYTAR=1
export MAINWP_APP_PASSWORD='your-application-password'
export MAINWP_DASHBOARD_URL='https://dashboard.example.com'
mainwpcontrol login --url https://dashboard.example.com --username admin
mainwpcontrol abilities list
```
`MAINWP_DASHBOARD_URL` is required alongside the password: the CLI releases the environment credential only to the Dashboard it names, and refuses when it is missing or points elsewhere.
2. **Or install C++ build tools** (`gcc`, `g++`, `make`) and reinstall.

## "command not found" after install
Expand Down
5 changes: 3 additions & 2 deletions docs/workflows/daily-health-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ You will be prompted for three pieces of information:
2. **Username:** Your WordPress admin username on the Dashboard site.
3. **Application Password:** The password you created in Step 1. Paste it in when prompted. The spaces in the password are fine; include them or omit them, both work.

After entering these, MainWP Control stores your credentials in your system's keychain when one is available (macOS Keychain, Linux secret service, or Windows Credential Manager). If the machine cannot use a keychain, keep `MAINWP_APP_PASSWORD` available in the environment for future runs.
After entering these, MainWP Control stores your credentials in your system's keychain when one is available (macOS Keychain, Linux secret service, or Windows Credential Manager). If the machine cannot use a keychain, keep `MAINWP_APP_PASSWORD` and `MAINWP_DASHBOARD_URL` available in the environment for future runs.

### Verify authentication

Expand Down Expand Up @@ -662,10 +662,11 @@ mkdir -p ~/.config/mainwpcontrol
nano ~/.config/mainwpcontrol/cron.env
```

Add this line, using the Application Password from Step 1 (spaces removed):
Add these lines, using the Application Password from Step 1 (spaces removed) and your Dashboard URL. The CLI releases the password only to the Dashboard named here, so both are required:

```bash
export MAINWP_APP_PASSWORD='your-app-password'
export MAINWP_DASHBOARD_URL='https://dashboard.example.com'
```

Save the file, then restrict its permissions so only you can read it:
Expand Down
2 changes: 1 addition & 1 deletion docs/workflows/input-from-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ MainWP Control will prompt you for three pieces of information:
2. **Username:** your WordPress admin username
3. **Application Password:** the password you created in Step 1

Enter each value when prompted. MainWP Control will test the connection and store the credentials in a local profile. If the machine cannot use the OS keychain, keep `MAINWP_APP_PASSWORD` available in the environment for future runs.
Enter each value when prompted. MainWP Control will test the connection and store the credentials in a local profile. If the machine cannot use the OS keychain, keep `MAINWP_APP_PASSWORD` and `MAINWP_DASHBOARD_URL` available in the environment for future runs.

### Verify authentication

Expand Down
5 changes: 3 additions & 2 deletions docs/workflows/monitoring-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ You will be prompted for three pieces of information:
2. **Username:** Your WordPress admin username on the Dashboard site.
3. **Application Password:** The password you created in Step 1. Paste it in when prompted. The spaces in the password are fine; include them or omit them, both work.

After entering these, MainWP Control stores your credentials in your system's keychain when one is available (macOS Keychain, Linux secret service, or Windows Credential Manager). If the machine cannot use a keychain, keep `MAINWP_APP_PASSWORD` available in the environment for future runs.
After entering these, MainWP Control stores your credentials in your system's keychain when one is available (macOS Keychain, Linux secret service, or Windows Credential Manager). If the machine cannot use a keychain, keep `MAINWP_APP_PASSWORD` and `MAINWP_DASHBOARD_URL` available in the environment for future runs.

### Verify authentication

Expand Down Expand Up @@ -706,7 +706,8 @@ Cron runs in a minimal environment and may not have access to your system keycha

```
MAINWP_APP_PASSWORD='your-app-password'
MAINWP_DASHBOARD_URL='https://dashboard.example.com'
*/5 * * * * /full/path/to/mainwp-metrics.sh
```

Replace `your-app-password` with the Application Password from Step 1 (spaces removed). Environment variables set at the top of the crontab apply to all jobs below them.
Replace `your-app-password` with the Application Password from Step 1 (spaces removed) and the URL with your Dashboard. Both are required: the CLI hands the environment credential only to the Dashboard named in `MAINWP_DASHBOARD_URL`. Environment variables set at the top of the crontab apply to all jobs below them.
8 changes: 6 additions & 2 deletions docs/workflows/monthly-batch-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ MainWP Control will prompt you for three pieces of information:
2. **Username:** Your WordPress admin username on that site.
3. **Application Password:** The password you created in Step 1.

After entering your credentials, MainWP Control stores them in a local profile so you do not need to re-enter them each time. If the machine cannot use the OS keychain, keep `MAINWP_APP_PASSWORD` available in the environment for future runs.
After entering your credentials, MainWP Control stores them in a local profile so you do not need to re-enter them each time. If the machine cannot use the OS keychain, keep `MAINWP_APP_PASSWORD` and `MAINWP_DASHBOARD_URL` available in the environment for future runs.

Verify that authentication is working:

Expand Down Expand Up @@ -524,6 +524,8 @@ jobs:
DASHBOARD_URL: ${{ secrets.DASHBOARD_URL }}
DASHBOARD_USER: ${{ secrets.DASHBOARD_USER }}
MAINWP_APP_PASSWORD: ${{ secrets.MAINWP_APP_PASSWORD }}
# Binds the password to one Dashboard; the CLI refuses to send it elsewhere.
MAINWP_DASHBOARD_URL: ${{ secrets.DASHBOARD_URL }}
steps:
- uses: actions/setup-node@v4
with:
Expand All @@ -546,7 +548,7 @@ jobs:
--username $DASHBOARD_USER
```

- `env`: Sets job-level environment variables so every `mainwpcontrol` step can authenticate. GitHub runners often do not persist credentials in an OS keychain between steps, so `MAINWP_APP_PASSWORD` must stay available for the whole job.
- `env`: Sets job-level environment variables so every `mainwpcontrol` step can authenticate. GitHub runners often do not persist credentials in an OS keychain between steps, so `MAINWP_APP_PASSWORD` must stay available for the whole job, together with `MAINWP_DASHBOARD_URL`, which pins the Dashboard it may be sent to.
- `${{ secrets.DASHBOARD_URL }}`: GitHub replaces this with the encrypted secret value at runtime. The actual value never appears in logs.
- The `>` after `run:` is YAML syntax for a folded string. It joins the following indented lines into a single command, which makes long commands easier to read.

Expand Down Expand Up @@ -612,6 +614,8 @@ jobs:
DASHBOARD_URL: ${{ secrets.DASHBOARD_URL }}
DASHBOARD_USER: ${{ secrets.DASHBOARD_USER }}
MAINWP_APP_PASSWORD: ${{ secrets.MAINWP_APP_PASSWORD }}
# Binds the password to one Dashboard; the CLI refuses to send it elsewhere.
MAINWP_DASHBOARD_URL: ${{ secrets.DASHBOARD_URL }}
steps:
- uses: actions/setup-node@v4
with:
Expand Down
8 changes: 6 additions & 2 deletions docs/workflows/plugin-deployment-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ MainWP Control will prompt you for three pieces of information:
2. **Username** -- Enter your WordPress admin username.
3. **Application Password** -- Paste the Application Password you created in Step 1.

After entering your credentials, MainWP Control stores them in a local profile so you do not have to enter them again on this machine. If the machine cannot use the OS keychain, keep `MAINWP_APP_PASSWORD` available in the environment for future runs.
After entering your credentials, MainWP Control stores them in a local profile so you do not have to enter them again on this machine. If the machine cannot use the OS keychain, keep `MAINWP_APP_PASSWORD` and `MAINWP_DASHBOARD_URL` available in the environment for future runs.

**Verify the connection:**

Expand Down Expand Up @@ -228,6 +228,8 @@ jobs:
DASHBOARD_URL: ${{ secrets.DASHBOARD_URL }}
DASHBOARD_USER: ${{ secrets.DASHBOARD_USER }}
MAINWP_APP_PASSWORD: ${{ secrets.MAINWP_APP_PASSWORD }}
# Binds the password to one Dashboard; the CLI refuses to send it elsewhere.
MAINWP_DASHBOARD_URL: ${{ secrets.DASHBOARD_URL }}
steps:
- uses: actions/setup-node@v4
with:
Expand Down Expand Up @@ -255,7 +257,7 @@ jobs:
--username $DASHBOARD_USER
```

- `env:` sets job-level environment variables so every `mainwpcontrol` step can authenticate. GitHub runners often do not persist credentials in an OS keychain between steps, so `MAINWP_APP_PASSWORD` must stay available for the whole job.
- `env:` sets job-level environment variables so every `mainwpcontrol` step can authenticate. GitHub runners often do not persist credentials in an OS keychain between steps, so `MAINWP_APP_PASSWORD` must stay available for the whole job, together with `MAINWP_DASHBOARD_URL`, which pins the Dashboard it may be sent to.
- `${{ secrets.NAME }}` is GitHub Actions syntax for reading a secret. GitHub replaces this with the actual value at runtime and automatically masks it in logs.
- `run: >` uses a YAML feature called **folding**. The `>` character means "join the following indented lines into a single line." This lets you split a long command across multiple lines for readability. The actual command that runs is: `mainwpcontrol login --url $DASHBOARD_URL --username $DASHBOARD_USER`
- The `--url` and `--username` flags provide credentials non-interactively, which is necessary because GitHub Actions runs without a terminal and cannot prompt for input.
Expand Down Expand Up @@ -326,6 +328,8 @@ jobs:
DASHBOARD_URL: ${{ secrets.DASHBOARD_URL }}
DASHBOARD_USER: ${{ secrets.DASHBOARD_USER }}
MAINWP_APP_PASSWORD: ${{ secrets.MAINWP_APP_PASSWORD }}
# Binds the password to one Dashboard; the CLI refuses to send it elsewhere.
MAINWP_DASHBOARD_URL: ${{ secrets.DASHBOARD_URL }}
steps:
- uses: actions/setup-node@v4
with:
Expand Down
Loading
Loading