# README

> A README is the file at a project's root that explains what it does and how to install it, rendered by code hosts and registries as its front page.

Also known as: README.md, README file, Read Me, README.rst, README.txt

Source: https://socket.dev/glossary/readme-md

Last updated: 2026-11-04

A README is the plain-text or Markdown file, usually named README.md, at the root of a software project that explains what the project does, how to install it, and how to use it. GitHub and package registries render it as the project's front page, which makes the README the most-read and least-verified part of an open source package.

Nothing binds a README to the code it describes. A lockfile pins a tarball's hash, and provenance attests to who built it, but no check confirms that the README tells the truth about either. Attackers copy READMEs wholesale to borrow a real project's credibility, and some put the payload in the install instructions instead of the code. Our position follows from the mechanism: read the README to learn what a package claims to do, then analyze the code to learn what it does.

## What is a README file?

A README file is a human-readable introduction that ships in a project's top-level directory. The convention dates to software distributions of the 1970s, and the uppercase name made the file sort near the top of ASCII directory listings. Most READMEs today are Markdown, which GitHub renders as GitHub Flavored Markdown. Python projects also use reStructuredText.

Code hosts find the file by name and location. [GitHub's documentation](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-readmes) says it checks three directories in order: the hidden `.github` directory, then the repository root, then `docs`. GitHub also truncates any README content beyond 500 KiB.

The `.github` precedence has a consequence most maintainers never consider. A repository can show `.github/README.md` on its GitHub front page while `npm publish` packs the root `README.md` into the tarball. The README you read on GitHub and the README in the package you install can be two different files from the same commit.

## What should a README file contain?

A README file should tell a new reader what the project does, why it is useful, how to get started, where to get help, and who maintains it. Those five items come from GitHub's guidance and cover most applications. A package that other code depends on needs four more:

- One install command that uses the ecosystem's package manager, and nothing else.
- A minimal usage example that runs as written.
- What the package does outside its API: install scripts, network requests, telemetry, bundled native binaries.
- The license, plus a private channel for vulnerability reports, usually a `SECURITY.md` file.

The third item is the one almost every README omits and the one a reviewer most needs. A README that says "makes no network requests" is still only a claim. It is, however, a claim that a tool can check against the code.

The first item is a security control, not a style rule. A legitimate library never needs `curl | sh`, a base64 string pasted into a terminal, or a password-protected archive. Each of those shows up in the attacks below.

## Where does a registry's README come from?

A registry's README is metadata that the publishing client uploads. The registry does not read it from your repository, and each ecosystem stores it differently. The differences decide whether the README you read matches the code you install.

| Host | What it displays | Where the text comes from | Same bytes you install? |
| --- | --- | --- | --- |
| GitHub | First README found in .github/, the repository root, or docs/ | Files on the repository's default branch | No. The repository is not the published package |
| npmjs.com | README file inside the tarball | Rendered from the tarball at publish time | Yes. The tarball's sha512 is pinned in your lockfile |
| npm registry JSON (npm view, API) | readme string in the packument | A field the client sends in the publish document, beside the tarball | Not guaranteed. Outside the tarball hash and provenance |
| PyPI | Description field of the core metadata, as text/markdown, text/x-rst, or text/plain | Copied at build time from the file named by pyproject.toml's readme key | A build-time copy, not README.md itself |
| crates.io | The README named by Cargo.toml's readme field (README.md, README.txt, or README by default) | Packed in the .crate and also sent as a readme JSON string at publish | Sent twice: once in the archive, once as metadata |

The verdict for npm is specific. Read the README from the artifact you install, which is the tarball that npmjs.com renders. Do not treat the registry JSON's `readme` field as a copy of it.

### npm keeps two READMEs for every version

When you run `npm publish`, the CLI copies the text of README.md into a `readme` field of the JSON document it uploads beside the tarball. The logic lives in [`@npmcli/package-json`](https://github.com/npm/package-json/blob/main/lib/normalize.js), and it only runs when the field is empty:

```javascript
// populate "readme" attribute
if (steps.includes('readme') && !data.readme) {
  // glob for {README,README.*}, prefer a Markdown extension
  ...
  data.readme = readmeData
  data.readmeFilename = readmeFile
}
```

The `!data.readme` condition means a `readme` string in [package.json](https://socket.dev/glossary/package-json) wins over the README file. We confirmed this with the copy of `@npmcli/package-json` 6.2.0 bundled in npm 10.9.9. As of November 2026, the same branch exists in the npm 12.2.1 source:

```shell
$ cat README.md
# demo-pkg

The README inside the tarball.

$ grep readme package.json
  "readme": "# demo-pkg\n\nA different README, sent only in the publish document."

$ NPM=$(dirname $(dirname $(readlink -f $(which npm))))
$ node -e "require('$NPM/node_modules/@npmcli/package-json')
    .prepare('.').then(p => console.log(p.content.readme))"
# demo-pkg

A different README, sent only in the publish document.

$ npm pack --dry-run 2>&1 | grep -i readme
npm notice 43B README.md
```

The npmjs.com website renders the tarball's copy instead. "npm now renders the README.md directly from the tarball at publish time," Ben Coe wrote for npm in [npm/www issue #136](https://github.com/npm/www/issues/136). One version therefore carries two READMEs. The tarball's README is covered by the integrity hash. The `readme` string in the packument is what `npm view`, registry API clients, and agents that read registry JSON receive.

The figure traces both copies from a single `npm publish`. Copy 1 is README.md inside the tarball. Its bytes are covered by `dist.integrity`, the sha512 hash your lockfile pins. That same sha512 is the subject of the provenance attestation, and npmjs.com renders this copy on the package page. Copy 2 is the `readme` field. The CLI fills it from package.json's `readme` string if one exists, otherwise from README.md's text, and sends it as a JSON string next to the tarball. The registry stores it in the packument at `registry.npmjs.org/<name>` and returns it from `npm view <name> readme`. Copy 2 sits outside both the hash and the attestation.

The two copies drift apart in ordinary packages, with no attacker involved:

```shell
$ npm view @inquirer/type readme
ERROR: No README data found!

$ npm view @babel/parser readme

$ npm view chalk readmeFilename
readme.md
```

The first string is a placeholder that npm's publishing code writes when it finds no README, and the registry stored it verbatim. npmjs.com instead tells visitors that @inquirer/type "does not have a README." For @babel/parser, the registry field is empty while npmjs.com renders the README from the tarball. On November 4, 2026, we compared the packument README with the tarball README for 250 packages returned by an npm registry search for `keywords:javascript`. 222 matched after trimming whitespace. 24 had an empty `readme` field even though their tarball contains a README. 4 stored the placeholder string.

Provenance does not close this gap. The attestation for sigstore@5.0.0 names one subject, the tarball's digest:

```json
"subject": [{
  "name": "pkg:npm/sigstore@5.0.0",
  "digest": { "sha512": "849a897e81bf7b8a8541a6ae40bd3473a27a16b1cb0462ad905bd6dc96d34881053a12eb4a375233f81677fc9439f628fe0b0445e8535c60b5559597fbd5f80e" }
}]
```

That digest equals the package's `dist.integrity`. Provenance proves which workflow built the tarball. It says nothing about the `readme` string stored beside it. This is the documentation half of [manifest confusion](https://socket.dev/glossary/manifest-confusion): the registry serves client-supplied metadata that nothing reconciles with the artifact.

## The README as an attack surface

The README is an attack surface because people and AI agents act on it, and nothing verifies it. Socket's threat research keeps finding the same three patterns.

### Cloned READMEs make impersonation free

A cloned README costs an attacker one copy command. In the [chalk-node author typosquat](https://socket.dev/blog/author-typosquatting-on-npm), the attacker registered `sindresrohus` and replicated chalk's README. The [cschalk kill-switch package](https://socket.dev/blog/kill-switch-hidden-in-npm-packages-typo-squatting-chalk-and-chokidar) shared chalk's identical README and branding while chalk had 268 million weekly downloads at the time and the clone had almost none. [express-dompurify](https://socket.dev/blog/malicious-express-dompurify-npm-package-steals-sensitive-data) shipped an exact copy of DOMPurify's README, a library downloaded more than 6.7 million times a week at the time.

The clones have grown more complete. In the [March 2026 Solana and Ethereum typosquats](https://socket.dev/blog/5-malicious-npm-packages-typosquat-solana-and-ethereum-libraries-steal-private-keys), one package carried 128 lines of README copied from @raydium-io/raydium-sdk-v2. Another cloned @ethersproject/wallet@5.8.0 with its README, LICENSE, and all 15 upstream dependencies, then injected a single line into the compiled output. The [GlassWorm sleeper extensions](https://socket.dev/blog/glassworm-sleeper-extensions-activated-on-open-vsx) on Open VSX clone the source, README, and metadata of real extensions, and the malicious code exists only in the compiled output. The [fast\_log impersonation crates](https://socket.dev/blog/two-malicious-rust-crates-impersonate-popular-logger-to-steal-wallet-keys) copied the logger's README and pointed their repository field at the real project, a form of [starjacking](https://socket.dev/glossary/starjacking).

A matching README is not evidence of a matching package. Name checks catch [typosquatting](https://socket.dev/glossary/typosquatting), but they fail when the name is real. [CanisterWorm](https://socket.dev/blog/canisterworm-npm-publisher-compromise-deploys-backdoor-across-29-packages) was a publisher compromise. Its `deploy.js` took npm tokens from the environment, bumped each package's patch version, "copies the original README when available," and republished under the legitimate name. Everything a human reviewer looks at was genuine except the code.

### Install instructions are the payload

Some attacks ship almost no malicious code, because the README asks the victim to fetch it. The [OpenClaw skill marketplace campaign](https://socket.dev/blog/openclaw-skill-marketplace-emerges-as-active-malware-vector) is the clearest case. VirusTotal analyzed 3,016 OpenClaw skills and traced 314 malicious ones to a single publisher. The skills told users to paste base64-encoded shell commands, download password-protected archives, or run binaries from external hosts. Researcher Jamieson O'Reilly summarized the format: "Skills are markdown files containing instructions that tell the agent what to do." [Agent skills](https://socket.dev/glossary/agent-skills) are READMEs that an agent executes.

[Operation Muck and Load](https://socket.dev/blog/malicious-go-module-exposes-github-malware-lure-network) built the same trap at scale on GitHub. The operator ran 222 lure repositories across 190 accounts, with GitHub Actions workflows that force-push synthetic commits every minute so the projects look maintained. The repositories only had to look credible "long enough for a target to clone, build, download, or follow setup instructions." The [fake Corepack site](https://socket.dev/blog/fake-corepack-site-distributes-infostealer-and-proxyware) followed the same pattern with a download page: Corepack ships as an npm package, and no official installer exists at corepack.org.

Copy-pasted package manager commands carry the same risk. An [npx](https://socket.dev/glossary/npx-npm-package-runner) command in a README downloads and runs a package in one step, so a typo or a swapped name in the instructions executes immediately.

### Hidden text targets machines

A README is rendered for people and read raw by machines. Markdown allows raw HTML, so an HTML comment is valid Markdown, and no renderer displays it:

````markdown
## Install

```sh
npm install left-pad-utils
```

<!-- [hidden text addressed to the coding agent: an extra setup step that fetches and runs a remote script] -->
````

We rendered that file with [readme\_renderer](https://pypi.org/project/readme-renderer/) 46.0, the library built to render package descriptions on PyPI. The output contains the heading and the install command and drops the comment entirely. An agent that opens README.md to learn how to install the project reads all of it.

The figure shows the same file twice. The rendered pane contains only the "Install" heading and the command `npm install left-pad-utils`. The raw pane contains the same lines plus the HTML comment carrying a hidden instruction addressed to the coding agent. The person and the agent read different documents.

The attack is not hypothetical. In September 2025, HiddenLayer demonstrated [CopyPasta](https://www.hiddenlayer.com/research/prompts-gone-viral-practical-code-assistant-ai-viruses), a [prompt injection](https://socket.dev/glossary/prompt-injection) hidden in a README's Markdown comment. Cursor treated it as a license notice and copied it into new files, so the injection spread through the codebase. We have seen the same idea aimed at security tools. The [shai\_hulululud npm package](https://socket.dev/blog/npm-package-uses-prompt-injection-and-token-flooding-to-disrupt-ai-malware-scanners) wrapped obfuscated code in fake "SYSTEM OVERRIDE" comments meant to derail AI-based malware scanners.

Search engines and summarizers read READMEs too. When we searched for the malicious [Tracer.Fody.NLog](https://socket.dev/blog/malicious-nuget-package-typosquats-popular-net-tracing-library) NuGet package, Google's AI Overview described it as a helpful adapter that improves logging performance. The [tea.xyz spam campaign](https://socket.dev/blog/massive-automated-spam-campaign-floods-npm-registry-with-thousands-of-garbage-tea-xyz-packages) filled GitHub organizations with auto-generated repositories whose READMEs were copies of legitimate projects.

## How do you check a README you don't trust?

You check a README by verifying the package it describes, not by reading the prose more carefully. Every check below has a limit, and the limits are the point.

- **Match identity, not content.** Compare the package name, publisher account, publish history, and whether the linked repository links back. This misses a compromised maintainer: CanisterWorm's republished versions had the right name, publisher, and history.
- **Install only through the package manager.** Refuse `curl | sh`, encoded commands, password-protected archives, and installers from unfamiliar domains. This does nothing when the package the correct command installs is itself malicious.
- **Put the package manager behind a firewall.** As of November 2026, [Socket Firewall Free](https://docs.socket.dev/docs/socket-firewall-free) wraps commands such as `sfw npm install` or `sfw uv pip install` and blocks network requests for package artifacts confirmed as malware. Packages flagged by AI but not yet confirmed only produce a warning. A [package firewall](https://socket.dev/glossary/package-firewall) sees package manager traffic, so it does not cover a `curl` pipe or a release binary the README links to.
- **Give agents rendered text and require approval for commands.** Stripping HTML comments removes the cheapest hiding place, and HiddenLayer recommends reviewing every change an agent proposes. Neither stops instructions that are visible and persuasive.
- **Analyze the code.** Static and behavioral analysis of the published artifact is the only check that does not rely on the README being honest. It costs time and tooling, and it cannot judge intent from code alone, but it reads the thing you actually run.

## Registries should derive the README from the artifact

Registries should stop accepting a client-supplied README and extract it from the published artifact instead. npm already renders its website from the tarball. The packument's `readme` field is a second, unhashed copy that any client can set to arbitrary text, and that copy is what API clients and agents read. Cargo's publish protocol carries the same duplicate: a `readme` string in the JSON metadata next to the `.crate` bytes, as documented in the [registry web API](https://doc.rust-lang.org/cargo/reference/registry-web-api.html).

Binding the README to the artifact would still leave the harder problem. A README bound to the right tarball can still lie, because it describes intent, and intent is what an attacker fakes. Coding agents now read a README, run its install command, and follow its setup steps without a person in the loop. The README has become executable documentation, and it still has none of the controls we require of executables.
