# Find Dead Code in a React Project

Yes. An agent lists the unused files, exports and dependencies in your React codebase, checking each one against the build and tests.

[Run this on Rush](https://cloud.getrush.ai/run/find-dead-code/react/).

## What the agent does

The agent clones the repository into its own cloud sandbox and reads how it is put together before running any tool, because a dead-code finder is only as right as its idea of the entry points. In a React codebase like excalidraw/excalidraw it opens the root `package.json` and finds `yarn@1.22.22` as the `packageManager` and three workspace globs: `excalidraw-app`, `packages/*` and `examples/*`. Under `packages/` it finds seven packages (`common`, `element`, `excalidraw`, `fractional-indexing`, `laser-pointer`, `math`, `utils`), each with its own `package.json` and `exports` map, and it notes that `@excalidraw/excalidraw` is published to npm at `0.18.0` with `packages/excalidraw/index.tsx` as its public entry.

For a TypeScript and React repository it uses knip, which reports unused files, unused exports and unused or unlisted dependencies per workspace. It writes a knip config that matches the repository instead of running the defaults. The root `tsconfig.json` maps every `@excalidraw/*` import to source with `paths`, and `vitest.config.mts` repeats those mappings as aliases, so the agent makes sure knip resolves them; without that, every cross-package import looks broken and every package export looks unused. It marks `packages/excalidraw/index.tsx` and each package's `exports` as entries, `excalidraw-app/index.tsx` as the app's entry, `setupTests.ts` as a test entry (it is the `setupFiles` entry in `vitest.config.mts`) and the scripts under `scripts/` as entries, since `build-node` and `buildPackage.js` are run from `package.json` rather than imported.

Then it treats knip's output as a list of suspects, not a verdict. Static analysis misses code reached through a side-effect import, a config file or a published API. In this repository `vitest-canvas-mock` and `fake-indexeddb` are only imported for their side effects in `setupTests.ts`, `rewire` is only required by `scripts/build-node.js`, and an export reached only through `index.tsx` is used by everyone who installs the npm package. So an export that is part of a published package's public surface is listed separately as public API to decide on, never as dead.

Each remaining candidate is checked by removing it on a scratch branch inside the sandbox and running what the repository already uses to catch breakage: `yarn test:typecheck` (plain `tsc` against the `noEmit` root config), `yarn test:code` (ESLint with `--max-warnings=0`), `yarn build:packages` and `yarn build` for the esbuild package builds and the Vite app build, and `yarn test:app --watch=false` for the Vitest suite. A dependency is checked by removing it from its workspace's `package.json`, reinstalling with `yarn install` and running the same commands. Anything that turns one of them red goes back, with the failing output, into a short section of things that looked dead and were not. Nothing is pushed and no pull request is opened.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Clone https://github.com/excalidraw/excalidraw. Find unused files, exports and package dependencies with knip, configured for this Yarn workspaces monorepo: resolve the @excalidraw/* path aliases from tsconfig.json and vitest.config.mts, and treat each package's exports, excalidraw-app/index.tsx, setupTests.ts and the scripts run from package.json as entry points. Then check each candidate by removing it on a scratch branch and running yarn test:typecheck, yarn test:code, yarn build:packages, yarn build and yarn test:app --watch=false. Write dead-code.md listing only what passed that check, grouped into files, exports and dependencies, each with its path and the command output that proved it. List exports of published packages separately as public API, and list every candidate that failed the check with the error. Do not push or open a pull request."
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is one file, `dead-code.md`, as an artifact on the session; reading the session returns a download link for it that works for an hour, and a fresh link on every read. It has three sections of things that are safe to delete: files nothing imports, exports nothing imports, and dependencies nothing requires, each line with its path, the workspace it belongs to and the commands that still passed with it removed. A fourth section lists exports that only a published package's entry reaches, so the maintainers can decide whether they are public API. A fifth lists the suspects that failed the check, with the error each one produced, which is usually where knip's config needs an extra entry point.

The proof sits beside the report. The session's transcript keeps every command the agent ran with its output, including the knip run and each delete-and-rebuild cycle, so a reviewer can open the step for any line in the report and see the type check, build and test output it rests on.

Before deleting anything, a reviewer checks the public API section against the package's changelog and semver promises, looks for code loaded by a path a build tool cannot see (a dynamic `import()` with a computed path, a file named in an HTML template or a deploy config), and confirms that the commands the agent ran are the ones the team's CI runs. The deletions themselves are a follow-up: send the session a message asking it to open a pull request that removes the confirmed items.

## Run it on every first Monday of the month

Dead code builds up slowly, so a monthly report is usually enough. Have a cron job on your server send this same request on the first Monday of each month, with an `Idempotency-Key` built from the date so a retried request does not start a second run. Each run starts from a fresh clone, so the report reflects the default branch as it is that day.

Set a webhook URL on the API key and Rush POSTs `run.completed` when the run's turn finishes, with the `session_url`. Your server reads the session from that URL, takes the `dead-code.md` link from its artifacts and posts it to the team channel or attaches it to a tracking issue.

## Why run it on Rush?

Proving that code is dead means deleting it and rebuilding, many times over. On a laptop that is an afternoon of stashing, reinstalling and waiting on a type check across seven packages, and it leaves the working tree in a state someone has to clean up. In a Rush sandbox the agent does every delete-and-rebuild cycle on its own disk, with its own `node_modules`, and nothing it removes touches your checkout or your branches.

The result also has to be trusted by someone who did not run it. A list from a static tool alone invites the question of whether each line was checked; this report comes with the transcript of every command and its output, so each claim that a file is safe to remove can be traced to a passing build and test run. Several of these can run at once, one per repository or per workspace, up to the key's concurrent run limit.

## Frequently asked questions

### Does it work on a private repository?

Yes. Install the Rush GitHub App on that repository and add it to the request as a resource, `{"url": "https://github.com/your-org/your-app"}`; the agent starts with the repository cloned. The example clones a public repository by URL, which needs no GitHub App at all.

### Will it delete anything from my repository?

No. Every deletion happens on a scratch branch inside the sandbox and is only used to run the build and tests. The prompt says not to push or open a pull request, and the result is a report you download. If you want the deletions, ask for a pull request in a follow-up message to the same session.

### Why not just run knip in CI?

Knip is a good finder, and the agent uses it. What it cannot tell you is whether removing a reported item breaks a build, a test or a side-effect import it could not see. The agent configures knip for your workspaces, then proves every candidate by removing it and running your real commands, and lists the ones that failed with their errors.

### What about exports that other projects use from my published package?

Those look unused from inside the repository, because the code that imports them lives elsewhere. The agent treats each package's `exports` and its entry file as entry points, and anything reached only through them is listed as public API for you to decide on, not as dead code.
