Repository navigation
Echo FAQ
Answers to common questions and fixes for common problems with Echo. If yours isn't here, check the CLI reference or open an issue (see the end of this page).
Mailpit, MailHog, Mailtrap and smtp4dev are good mail catchers. If all you need is an inbox for one app's SMTP mail, use one. Echo has an inbox too (Echo Inbox) and does three things they don't:
-
It compares. A catcher shows you an inbox. Echo records what each action sent, then tells you after an update that the office's copy of the contact form is gone, the SMS now has
{first_name}in it and the Zapier payload lost a field. - Every channel, not only SMTP. Most of what a modern site sends isn't SMTP at all: API mailers (SendGrid, Postmark, Resend), SMS, WhatsApp, Slack, webhooks, GA4 and Meta events, payment calls. Echo catches them through the connector or its proxy, recognizes 53 services and answers each one the way the service would, so the site carries on.
- It seals. Pointing the mailer at a catcher stops email. It doesn't stop the SMS plugin, the WooCommerce webhook to the live CRM or the purchase event that feeds your ads. Echo's seal covers them and refuses live payment calls in every mode.
rhythm echo up also answers Mailpit's API for email (/api/v1/messages, /api/v1/search and friends), so tests written against Mailpit's API keep working. Its image drops in where Mailpit runs in DDEV, Lando and Sail; see API and Docker and DDEV.
check and headers are: they only read DNS and files. Everything else is for staging and local sites. The connectors do nothing in production (WP_ENVIRONMENT_TYPE, APP_ENV, NODE_ENV, VERCEL_ENV). List your live sites under production: (["shop.com"] covers its subdomains too): snapshot, compare, run and replays then refuse them before sending anything and doctor fails when site: is one of them.
The running catcher keeps the newest 10,000 messages in memory and forgets them when it stops, unless you start it with --store <dir>, which keeps them across restarts (see Echo Inbox). What actions sent is saved in .echo/snapshots/ and .echo/runs/, next to the config; rhythm echo report shows it again.
Work down this list:
-
Is the connector on? Run
rhythm echo doctor.the connector is installed but off: productionmeans the site thinks it's production: on WordPress, setWP_ENVIRONMENT_TYPEtostaging,developmentorlocal(WordPress treats unset as production). For Laravel it'sAPP_ENV; for Node,NODE_ENV=productionoutside Vercel (whichnext startsets: addRHYTHM_ECHO_FORCE=1on a staging server) or an unsetNODE_ENVwithoutRHYTHM_ECHO_URL; the Node connector logs[rhythm-echo] off: …with the reason. -
Can it reach Echo?
active but it can't reach Echo at …means the URL it uses is wrong from where the site runs; see The site runs in Docker. While Echo is unreachable, a sealed connector refuses to send and logs why; a comparison lists the action as failed and each of its messages as missing. -
Does it go through a hook the connector patches? On WordPress, that's
wp_mail()andwp_remote_*(). SDKs that call cURL themselves (the Twilio and Stripe PHP SDKs, WP Mail SMTP's Gmail mailer) don't: add the proxy. In Laravel,new GuzzleHttp\Client()and SDKs with their own HTTP stack; in Node, a separateundici,http2or raw sockets. The proxy covers those too. -
Did something replace
wp_mail()before Echo loaded?doctorshows the warning wp_mail() was already defined (…). Echo then catches mail throughpre_wp_mailandphpmailer_init, which a replacement may skip. Find the plugin in the warning's path. -
Is it sent later? WooCommerce webhooks, many notifications and anything queued go out with WP-Cron, Action Scheduler or a queue worker, after the action is over. Add a
flush:command; see Flush queued work. -
Did it arrive outside any action?
rhythm echo lsshows everything. A message outside any action came without a run ID, after the action closed. Raisequiet, or make sure the request that triggers it carriesX-Echo-Run. -
Was its run ID ignored? A connector logs
ignored an X-Echo-Run …when the run value isn't signed with its token, or came through a proxy without one. Set the same token in the connector and in Echo; see Run IDs and signatures.
Platform update checks (api.wordpress.org, repo.packagist.org) are let through without being recorded, on purpose; see What passes through.
Inside a container, 127.0.0.1 is the container itself, so a connector pointed at http://127.0.0.1:8025 can't reach Echo on the host. Two parts to fix:
The simplest fix is to run Echo from its image in a container on the same network, then point the site at http://echo:8025; see Docker and DDEV. To keep Echo on the host instead, there are two parts:
-
Make Echo listen where the container can reach it. On Linux, that's the Docker bridge's address on the host (
172.17.0.1for the default bridge;ip -4 addr show docker0). It's beyond loopback, so Echo needs a token of 16 characters or more (or--private-network, since the bridge is a private network):export RHYTHM_ECHO_TOKEN=$(openssl rand -hex 16) rhythm echo up --listen 172.17.0.1:8025 --smtp 172.17.0.1:1025
The SMTP sink then wants a login: any username, with the token as the password. Give the site's mailer those settings (the WordPress connector doesn't need them: it reports mail through the API).
With Docker Desktop (macOS, Windows),
host.docker.internalreaches the host's loopback, so Echo can stay on127.0.0.1. -
Point the connector at the host. Start the container with
--add-host host.docker.internal:host-gateway(orextra_hostsin Compose), then install with that URL and the token:rhythm echo install wordpress --container shop-cli --url http://host.docker.internal:8025 --token "$RHYTHM_ECHO_TOKEN"
Put the same address in
.echo/config.yaml(listen: 172.17.0.1:8025,smtp: 172.17.0.1:1025), sodoctorand your actions find Echo. When the install uses--containerand a127.0.0.1URL, Echo reminds you:In Docker, 127.0.0.1 and localhost are the container itself: reinstall with --url http://host.docker.internal:8025.
For a stack that's all in Compose, run Echo as a service instead; the hard seal recipe does that and seals the network too. Installing into the official wordpress image, which has no WP-CLI, is covered in WordPress in Docker.
On a terminal, a failed check starts with ✗ and a note with ·; with --plain they start with FAIL and note. Passed checks start with ok.
| Line | Means | Fix |
|---|---|---|
✗ Echo: no Echo is running at http://127.0.0.1:8025 (start it with: rhythm echo up) |
Nothing answered at the URL | Start Echo, or pass --url / set listen: to where it runs. |
✗ Echo: http://127.0.0.1:8025 is Mailpit, not Echo; start Echo on a free port (rhythm echo up --listen 127.0.0.1:18025) and set listen: in .echo/config.yaml to it |
Mailpit has the port | Run Echo next to it (rhythm echo up --free-ports), or stop Mailpit. |
· Payments: live calls refused for Stripe, PayPal, … |
A note, not a check | Nothing: live payment calls are refused in every mode. |
✗ SMTP: 127.0.0.1:1025: … (is Echo running with this config?) |
The sink isn't where the config says | Run doctor from the same folder as up, with the same config. |
· WordPress: no connector answered at … (…) |
The site has no connector, the URL is wrong, or the REST route is blocked (a security plugin, basic auth) | Install it; check site:; allow ?rest_route=/rhythm-echo/v1/ping from Echo's machine. |
✗ WordPress: the connector is installed but off: production |
The production guard | Set WP_ENVIRONMENT_TYPE. |
✗ WordPress: active but it can't reach Echo at … (in Docker use host.docker.internal) |
The connector's URL is wrong from the site | Reinstall with the right --url. |
✗ Canaries: only 1 of 2 canaries arrived |
One layer isn't catching | The email or HTTP hook is bypassed; see the warnings on the · lines. |
✗ Laravel: … |
php artisan echo:canary failed |
Run it yourself to see the error; check RHYTHM_ECHO_URL in .env and php artisan config:clear. |
✗ Node: @rhythmsh/echo isn't installed in this folder; add it: rhythm echo install node |
A Next.js or Node project without the connector | Install the package. the canary didn't arrive in Echo means the connector is on but can't reach Echo. |
· Stack: no WordPress, Laravel, Next.js, Node or PHP project in this folder, so no connector was checked; connect one: rhythm echo install |
doctor found no stack where .echo/ is |
Run it from the project, or set site:. |
✗ Flush: "…": exit status 1 … |
A flush command fails | Run it by hand from the same folder. |
✗ Site: http://staging.shop.test matches "shop.test" under production: snapshot, compare and run refuse it before any action (command actions too) and replays never go to it; point site: at a staging copy |
site: is under a production: entry |
Point site: at staging, or narrow the entry. |
PHP's opcache may keep serving the old file for a moment: the official wordpress images set opcache.revalidate_freq=2, so a changed file is picked up within two seconds. Some hosts set it much higher, or turn validate_timestamps off; then reload PHP-FPM after installing or uninstalling.
With --mitm, the proxy presents certificates from Echo's own CA, so each client must trust it. Most read the system store or an environment variable (see the table). Some ship their own CA bundle and ignore all of these:
-
Stripe's PHP SDK uses its own
data/ca-certificates.crt. Call\Stripe\Stripe::setCABundlePath('/etc/rhythm-echo/bundle.pem'). Allowlistingapi.stripe.comdoesn't help here: payment APIs are never tunnelled blind. With--mitmEcho still decrypts Stripe calls (to tell test keys from live ones); without it, Echo refuses them. -
WordPress verifies against
wp-includes/certificates/ca-bundle.crt: install the connector with--ca, which adds Echo's CA to that bundle. -
Python's
certifi(used byrequests): setREQUESTS_CA_BUNDLEto a bundle of the system CAs plus Echo's. -
Java keeps its own keystore: import
ca.pemwithkeytool -importcert.
A client that replaces its store with Echo's certificate alone breaks for allowed hosts, which present their real certificates; build a bundle:
cat /etc/ssl/certs/ca-certificates.crt "$(rhythm echo ca)" > /etc/rhythm-echo/bundle.pemWhen the TLS handshake fails, the client usually says self-signed certificate in certificate chain (curl's error 60). Nothing is recorded.
An action with expect: ends as soon as what it expects has arrived. When something is missing, it waits up to the full timeout (30 s by default) for it before reporting Sent fewer emails than expected. Its progress line shows ✗ and fewer than expected. That's the expect failing, not Echo hanging. Lower timeout (or pass --timeout 10s) if your messages arrive quickly.
DDEV, Lando and Sail run Mailpit on 8025 and 1025. rhythm echo up names it and offers the next free pair; --free-ports takes it without asking and saves it to the config. Or replace Mailpit with Echo's image. See Docker and DDEV.
An action failed in this run (its request errored, its command exited non-zero), so what it sends wasn't compared: the verdict says Could not compare: 1 of 2 actions failed in this run instead of claiming nothing changed. The action is BROKE run/action-failed. Fix the action first. When every action failed, a snapshot isn't saved (not saved: every action failed; fix the site or pass --force) and the command exits 3.
Usually a value that changes on every run reaches a message: a real address typed into an action, a random reference in the subject. Use ${fake:email}, ${fake:name} and ${fake:phone} in actions; Echo recognizes them as test data. Check what differs with rhythm echo report --plain, which shows each text change as a diff. Dates, times, order and invoice numbers, coupon codes and token-like strings are already ignored; see How messages are matched. For a payload field that differs on purpose, add it to the action's ignore:; for a change you meant, compare --accept <key>.
Commands find the config in the folder you're in or one above it. They skip one in a folder that isn't yours or that anyone can write to, such as /tmp, since its commands would run in your shell. Pass --config <path> if you trust it, or move the project. See How it's found.
--snapshot (or report <name>) names a snapshot that isn't there; rhythm echo snapshots lists them. Deleting the latest snapshot is fine: the newest remaining one becomes the latest.
An action was renamed, removed, or left out with --action. Actions are matched by name, so a renamed action isn't compared with its old self: its messages show as NEW, with 1 action isn't in the snapshot; what it sent is listed as new. Take a new snapshot after renaming.
Problem titles use the services' display names (Amazon SES email, Zapier webhook, request to <host>). An older build used other names; 1.1 recognizes services 1.0 recorded as generic webhooks. Compared with a snapshot taken before that, the same problems can show as NEW and FIXED. Take a new snapshot. A snapshot taken by Echo 1.0 still loads and compares.
Each refusal says which rule it hit; see Safety rules. The most common is a target that resolves to a public address: add the staging URL under staging:.
Open an issue on the Echo repository with:
-
rhythm echo --version, your OS and the stack and connector (WordPress, Laravel or Node version); - the command you ran and its output, with
--plain; -
rhythm echo doctoroutput; - for a missed or misrecognized message, the request as
rhythm echo show <id>prints it, with real data removed.
Use your own test site, never a third-party one.
Don't open a public issue. Report it privately at https://github.com/RhythmShahriar/echo/security/advisories/new. See the Security policy.
← CLI reference · Home · Benchmark →
rhythm · Useful ideas. Working systems. · Check only sites you own or have permission to test. Keep staging sealed (responsible use) · This wiki is published from wiki/ in the rhythm repository; suggest changes there with a pull request.
Get started
rhythm in depth
Tuner
- Overview
- Your first scan
- Reading results
- Choosing checks
- Requests and politeness
- Output and reports
- Batch scans
- Local scans
- Integrity and malware
- Changes and notifications
Tuner in depth
- Known vulnerabilities
- PHP advisories
- CI and code scanning
- GitHub Action
- Config file
- Watch and serve
- Writing checks
- Check packs
- Finding reference
- CLI reference
- Coming from Wordfence CLI or WPScan
- FAQ and troubleshooting
Soundcheck
- Overview
- Your first check
- Reading results
- Review and accept
- Discovery
- Page health
- Visual changes
- Interactions
- Forms and flows
- Cache
- Speed
- Accessibility and SEO
Soundcheck in depth
- CI
- CI baselines and PR comments
- Configuration
- Error rules and exec
- WordPress updates
- Fleet and client reports
- History
- Import from BackstopJS and Lost Pixel
- Firefox and WebKit
- CLI reference
- FAQ and troubleshooting
- Benchmark
Echo
- Overview
- Your first run
- Connectors
- Sealing staging
- Echo Inbox
- Messages and checks
- Checks and rules
- Attachments
- Actions and compare
- With Soundcheck
- Replay
- Deliverability
Echo in depth
- Docker and DDEV
- Agency hub and reports
- Schedule
- MCP server
- Conformance kit
- CI
- Configuration
- API
- CLI reference
- FAQ and troubleshooting
- Benchmark
Project